SWIG如何在可从Python调用的结构中创建typedef函数指针

时间:2018-11-21 15:05:54

标签: python c function struct swig

TL; DR 有人知道如何指示SWIG将C结构的这些成员视为函数指针,并使其可从Python调用吗?

完整故事 我有C结构,其中包含指向函数的指针。这些函数都是类型定义的。我有一个C函数,它将为此C结构分配内存,并将函数指针设置为指向有效的C函数。 我的简化头文件看起来像这样

// simplified api.h
typedef void *handle_t;
typedef void sample_t;
typedef error_t comp_close_t(handle_t *h);
typedef error_t comp_process_t(handle_t h,
                               sample_t *in_ptr,
                               sample_t *out_ptr,
                               size_t *nr_samples);
typedef struct
{
    comp_close_t *close;
    comp_process_t *process;
} audio_comp_t;

// prototype for init
error_t comp_init(handle_t *h, int size);

以及相应的简化源文件:

// simplified api.c
static comp_close_t my_close;
static comp_process_t my_process;

audio_comp_t comp = {
    my_close,
    my_process
};

error_t comp_init(audio_comp_t **handle) {
    *handle = ∁
    return 0;
}

error_t my_close(handle_t *h) {
    // stuff
    *h = NULL;
    return 0;
}

error_t my_process(handle_t h,
                   sample_t *in_ptr,
                   sample_t *out_ptr,
                   size_t *nr_samples) {
    audio_comp_t *c = (audio_comp_t*) h;
    // stuff 
    printf("doing something useful\n");
}

以及我的界面文件的最新版本:

%module comp_wrapper
%{
#include "api.h"
%}

%include "api.h"

// Take care of the double pointer in comp_init
%ignore comp_init;
%rename(comp_init) comp_init_overload;
%newobject comp_init;

%inline %{
audio_comp_t* comp_init_overload(int size) {
    audio_comp_t *result = NULL;
    error_t err = comp_init(&result, size);

    if (SSS_NO_ERROR == err) {
        ...
    }

    return result;
}
%}

// wrap the process call to verify the process_t * function pointer    
%inline %{
sss_error_t call_process(   audio_comp_t *h, 
                            sample_t *in, 
                            sample_t *out, 
                            size_t nr_samples)
{
    return h->process(h, in, out, &nr_samples);
}
%}

我想使用SWIG创建语言绑定,以便可以用最少的Python样板代码调用这些类似对象的结构。最终,我想这样使用:

h = comp_init(50)
h.process(h, input_data, output_data, block_size)
h.close(h)

但是,SWIG将这些结构中的这些函数指针视为对象,因此每当我要调用它们时,我都会得到

>>> h = comp_init(50)
>>> h.api.process()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'SwigPyObject' object is not callable

我可以通过接口文件中的“ call_process”函数来解决该问题:

call_process(h, in, out, 32) 

但是它会要求我为所有结构成员函数添加一个额外的包装器,但这不是必需的,因为[SWIG文档指出功能指针完全受支持] [1]

我假设我应该在接口文件中编写一些代码,以便SWIG知道它在处理函数而不是SwigPyObject

有一些有关如何处理(python)回调的信息,但是在这种情况下,似乎没有一种有效: SWIG call function pointers stored within struct

或不将或多或少的所有信息从头文件复制到接口文件中: Using SWIG with pointer to function in C struct

最后但并非最不重要的一点是,当您将函数指针包装在struct中时似乎有所不同,因此解决方案5不起作用: How to wrap a c++ function which takes in a function pointer in python using SWIG

有人知道如何指示SWIG将C结构的这些成员视为函数指针并使其可从Python调用吗?

1 个答案:

答案 0 :(得分:2)

最简单的解决方案是,如果我们向SWIG声明function pointers are simply member functions,那么它将生成的包装程序效果很好。

为了证明在这种情况下,我们需要修复示例代码中的一些错误,因此我最终得到api.h,如下所示:

// simplified api.h
#include <stdint.h>
#include <stdlib.h>

typedef uint32_t api_error_t;

typedef void *handle_t;
typedef void sample_t;
typedef api_error_t comp_close_t(handle_t h);
typedef api_error_t comp_process_t(handle_t h,
                               sample_t *in_ptr,
                               sample_t *out_ptr,
                               size_t nr_samples);
typedef struct
{
    comp_close_t *close;
    comp_process_t *process;
} audio_comp_t;

// prototype for init
api_error_t comp_init(handle_t *new_h);

和api.c看起来像这样:

#include "api.h"
#include <stdio.h>

// simplified api.c
static comp_close_t my_close;
static comp_process_t my_process;

audio_comp_t comp = {
    my_close,
    my_process
};

api_error_t comp_init(handle_t *handle) {
    *handle = &comp;
    return 0;
}

api_error_t my_close(handle_t h) {
    (void)h; // stuff
    return 0;
}

api_error_t my_process(handle_t h,
                   sample_t *in_ptr,
                   sample_t *out_ptr,
                   size_t nr_samples) {
    audio_comp_t *c = (audio_comp_t*) h;
    (void)c;(void)in_ptr;(void)out_ptr;// stuff 
    printf("doing something useful\n");
    return 0;
}

有了这个,我们可以编写如下的api.i:

%module api

%{
#include "api.h"
%}

%include <stdint.i>

%typemap(in,numinputs=0) handle_t *new_h (handle_t tmp) %{
    $1 = &tmp;
%}

%typemap(argout) handle_t *new_h %{
    if (!result) {
        $result = SWIG_NewPointerObj(tmp$argnum, $descriptor(audio_comp_t *), 0 /*| SWIG_POINTER_OWN */);
    }
    else {
        // Do something to make the error a Python exception...
    }
%}

// From my earlier answer: https://stackoverflow.com/a/11029809/168175
%typemap(in,numinputs=0) handle_t self "$1=NULL;"
%typemap(check) handle_t self {
  $1 = arg1;
}

typedef struct {
  api_error_t close(handle_t self);
  api_error_t process(handle_t self,
                      sample_t *in_ptr,
                      sample_t *out_ptr,
                      size_t nr_samples);
} audio_comp_t;

%ignore audio_comp_t;
%include "api.h"

除了隐藏原始结构并声明其已包含成员函数而不是成员指针之外,我们还做了一些其他事情:

  1. 使用SWIG automatically pass the handle in as the 1st argument,而不是要求Python用户过于冗长。 (在python中,它变成h.close()而不是h.close(h)
  2. 使用argout类型映射包装实际的comp_init函数,而不是仅仅替换它。纯粹是出于偏好的问题,我只是添加了它以说明如何使用它。

这使我可以运行以下Python:

import api

h=api.comp_init()
print(h)
h.process(None, None, 0)
h.close()

如果您愿意对API标头进行一些外观上的更改以方便操作,那么我们可以做一些对Python和C都很好的工作。

我在api.h MAKE_API_FUNC中引入了一个宏,该宏包装了您原来在其中的typedef语句。使用C编译器进行编译时,它仍然会产生完全相同的结果,但是它使我们可以使用SWIG更好地进行操作。

所以api.h现在看起来像这样:

// simplified api.h
#include <stdint.h>
#include <stdlib.h>

typedef uint32_t api_error_t;

typedef void *handle_t;
typedef void sample_t;
#ifndef MAKE_API_FUNC
#define MAKE_API_FUNC(name, type, ...) typedef api_error_t comp_ ## name ## _t(__VA_ARGS__)
#endif

MAKE_API_FUNC(close, audio_comp_t, handle_t);
MAKE_API_FUNC(process, audio_comp_t, handle_t, sample_t *, sample_t *, size_t);

typedef struct
{
    comp_close_t *close;
    comp_process_t *process;
} audio_comp_t;

// prototype for init
api_error_t comp_init(handle_t *new_h);

因此,在api.i中,我们现在将该宏替换为另一个宏,该宏向SWIG声明,函数指针typedef实际上是一个结构,并带有专门提供的__call__函数。通过创建此额外的函数,我们可以将所有Python参数自动代理到对真实函数指针的调用中。

%module api

%{
#include "api.h"
%}

%include <stdint.i>

// From: https://stackoverflow.com/a/2653351
#define xstr(a) str(a)
#define str(a) #a

#define name_arg(num, type) arg_ ## num
#define param_arg(num, type) type name_arg(num, type)

#define FE_0(...)
#define FE_1(action,a1) action(0,a1)
#define FE_2(action,a1,a2) action(0,a1), action(1,a2)
#define FE_3(action,a1,a2,a3) action(0,a1), action(1,a2), action(2,a3)
#define FE_4(action,a1,a2,a3,a4) action(0,a1), action(1,a2), action(2,a3), action(3,a4)
#define FE_5(action,a1,a2,a3,a4,a5) action(0,a1), action(1,a2), action(2,a3), action(3,a4), action(4,a5)

#define GET_MACRO(_1,_2,_3,_4,_5,NAME,...) NAME
%define FOR_EACH(action,...)
  GET_MACRO(__VA_ARGS__, FE_5, FE_4, FE_3, FE_2, FE_1, FE_0)(action,__VA_ARGS__)
%enddef

%define MAKE_API_FUNC(name, api_type, ...)
%nodefaultctor comp_ ## name ## _t;
%nodefaultdtor comp_ ## name ## _t;
typedef struct {
    %extend {
        api_error_t __call__(FOR_EACH(param_arg, __VA_ARGS__)) {
            return $self(FOR_EACH(name_arg, __VA_ARGS__));
        }
    }
} comp_ ## name ## _t;
// Workaround from: https://github.com/swig/swig/issues/609
%rename("%s_fptr", "%$isvariable", "match$ismember"="1", "match$type"=xstr(comp_ ## name ## _t)) name;
%extend api_type {
    %pythoncode %{
        name = lambda self, *args: self.name ## _fptr(self, *args)
    %}
}
%enddef

%ignore comp_init;
%include "api.h"

%extend  audio_comp_t {
    audio_comp_t() {
        handle_t new_h = NULL;
        api_error_t err = comp_init(&new_h);
        if (err) {
            // throw or set Python error directly
        }
        return new_h;
    }

    ~audio_comp_t() {
        (void)$self;
        // Do whatever we need to cleanup properly here, could actually call close
    }
}

这使用的是与preprocessor mechanisms中使用的相同的my answer on wrapping std::function objects,但适用于此问题的函数指针。另外,从Python的角度来看,我使用了%extend to make a constructor/destructor,这使该API更易于使用。如果这是真实代码,我可能也会使用%rename

话虽如此,我们现在可以使用以下Python代码:

import api

h=api.audio_comp_t()
print(h)
print(h.process)
h.process(None, None, 0)

请参见SWIG docs,以获取有关如何很好地将错误代码映射到异常上的Python讨论。


我们可以通过一个简单的技巧,通过消除对可变参数宏的参数进行迭代的需要来进一步简化此操作。如果我们将api.h宏更改为采用3个参数,则其中第3个是所有函数指针的参数,如下所示:

// simplified api.h
#include <stdint.h>
#include <stdlib.h>

typedef uint32_t api_error_t;

typedef void *handle_t;
typedef void sample_t;
#ifndef MAKE_API_FUNC
#define MAKE_API_FUNC(name, type, args) typedef api_error_t comp_ ## name ## _t args
#endif

MAKE_API_FUNC(close, audio_comp_t, (handle_t self));
MAKE_API_FUNC(process, audio_comp_t, (handle_t self, sample_t *in_ptr, sample_t *out_ptr, size_t nr_samples));

typedef struct
{
    comp_close_t *close;
    comp_process_t *process;
} audio_comp_t;

// prototype for init
api_error_t comp_init(handle_t *new_h);

然后,我们现在可以更改SWIG接口,以不提供通过__call__添加的%extend函数的定义,而是编写一个直接使我们想要的函数指针调用的宏:

%module api

%{
#include "api.h"
%}

%include <stdint.i>

// From: https://stackoverflow.com/a/2653351
#define xstr(a) str(a)
#define str(a) #a

%define MAKE_API_FUNC(name, api_type, arg_types)
%nodefaultctor comp_ ## name ## _t;
%nodefaultdtor comp_ ## name ## _t;
%{
#define comp_ ## name ## _t___call__(fptr, ...) fptr(__VA_ARGS__)
%}
typedef struct {
    %extend {
        api_error_t __call__ arg_types;
    }
} comp_ ## name ## _t;
// Workaround from: https://github.com/swig/swig/issues/609
%rename("%s_fptr", "%$isvariable", "match$ismember"="1", "match$type"=xstr(comp_ ## name ## _t)) name;
%extend api_type {
    %pythoncode %{
        name = lambda self, *args: self.name ## _fptr(self, *args)
    %}
}
%enddef

%ignore comp_init;
%include "api.h"

%extend  audio_comp_t {
    audio_comp_t() {
        handle_t new_h = NULL;
        api_error_t err = comp_init(&new_h);
        if (err) {
            // throw or set Python error directly
        }
        return new_h;
    }

    ~audio_comp_t() {
        (void)$self;
        // Do whatever we need to cleanup properly here, could actually call close
    }
}

这里棘手的事情是,typedef struct {...} name;惯用法的使用使重命名或隐藏结构体内的函数指针变得更加困难。 (不过,这只是保持自动添加handle_t参数的必要条件。)