如何使用 SWIG 包装带有可变参数的 C 函数

Posted

技术标签:

【中文标题】如何使用 SWIG 包装带有可变参数的 C 函数【英文标题】:How to wrap a C function with variable arguments using SWIG 【发布时间】:2018-05-28 10:01:50 【问题描述】:

我正在尝试使用 SWIG 包装一个带有可变参数的 C 函数,如下所示。

void post(const char *fmt, ...)

    char buf[MAXPDSTRING];
    va_list ap;
    t_int arg[8];
    int i;
    va_start(ap, fmt);
    vsnprintf(buf, MAXPDSTRING-1, fmt, ap);
    va_end(ap);
    strcat(buf, "\n");

    dopost(buf);

但是当我在 Lua 中运行该函数时,它仅在我使用 1 个参数时才有效。 我无法用以下风格写作。

pd.post("NUM : %d", 123);

我收到以下错误。

Error in post expected 1..1 args, got 2

是否可以使用 SWIG 包装带有可变参数的 C 函数?

如果有任何帮助,我将不胜感激。谢谢!

【问题讨论】:

swig.org/Doc1.3/Varargs.html @HenriMenke 谢谢,我阅读了链接,但我仍然不明白如何包装函数以及是否可以在 Lua 中使用 SWIG。并且该链接只有 Python 示例。我希望得到更详细的答案。 正如您在此页面中看到的,没有明智的方法来包装 C 可变参数函数,除非您愿意为该函数编写大量样板文件和您自己的包装器,从而违背了 SWIG 的目的。也许你应该重新考虑你的设计。 我也许可以一起破解一些东西和/或为 SWIG 编写一个可以正确执行此操作的补丁。您是否能够执行以下任一操作:a)添加在 va_list 上运行的 post 版本或 b)完全限制您需要支持的 ABI 的数量(例如,仅 linux/x86),这将稍微简化添加 varargs支持lua后端。 @Flexo a) 是 b) 不,我不想限制要支持的 ABI 的数量。 【参考方案1】:

免责声明: 不是真正的答案,因为我没有找到覆盖 SWIG 的参数检查的方法,所以我可以自己处理可变参数。这可以通过结合我下面展示的方法和this answer来解决。

链接和进一步阅读

Variable Length Arguments (a.k.a, "The horror. The horror.") 在 SWIG 文档中 Create va_list dynamically 堆栈溢出 How do I fill a va_list 堆栈溢出

纯 C 封装

我准备了一个示例,如何使用 libffi (documentation) 将对可变 Lua 函数的调用转换为可变 C 函数。

目前该代码仅处理 int(需要 Lua 5.3)、doubleconst char * 参数。它也可以简单地扩展到更多类型。请记住,这种方法非常不安全。使用不受支持的格式将导致分段错误(格式字符串未选中)。例如针对 Lua 5.2 进行编译并尝试像这样使用整数格式

printf("Hello World! %d %d %d\n", 1, 5, 7)

会导致

Hello World! 0 0 0

如果你很幸运并且它不会对你产生段错误,但是在像 valgrind 这样的内存调试器中运行程序会发现你正在做一些令人讨厌的事情。

// clang -Wall -Wextra -Wpedantic -std=c99 -g -I/usr/include/lua5.3 test.c -llua5.3 -lffi

#include <stdio.h>
#include <stdlib.h>

#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

#include <ffi.h>

static int l_printf(lua_State *L) 
    typedef union 
        int integer;
        double number;
        const char *string;
     variant;

    int argc = lua_gettop(L);
    variant *argv = malloc(argc * sizeof(variant));

    ffi_cif cif;
    ffi_type **types = malloc(argc * sizeof(ffi_type *));
    void **values = malloc(argc * sizeof(void *));

    for (int i = 0; i < argc; ++i) 
        int j = i + 1;
        switch (lua_type(L, j)) 
        case LUA_TNUMBER:
#if LUA_VERSION_NUM >= 503
            if (lua_isinteger(L, j)) 
                types[i] = &ffi_type_sint;
                argv[i].integer = lua_tointeger(L, j);
                values[i] = &argv[i].integer;
             else
#endif
            
                types[i] = &ffi_type_double;
                argv[i].number = lua_tonumber(L, j);
                values[i] = &argv[i].number;
            
            break;
        case LUA_TSTRING:
            types[i] = &ffi_type_pointer;
            argv[i].string = lua_tostring(L, j);
            values[i] = &argv[i].string;
            break;
        default:
            puts("Unhandled argment type");
            abort();
            break;
        
    

    // If preparing the FFI call fails we simply push -1 to indicate
    // that printf failed
    int result = -1;
    if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, argc, &ffi_type_sint, types) ==
        FFI_OK) 
        ffi_call(&cif, (void (*)())printf, &result, values);
    

    free(values);
    free(types);
    free(argv);

    lua_pushinteger(L, result);
    return 1;


int main(int argc, char *argv[]) 
    if (argc != 2) 
        fprintf(stderr, "Usage: %s <script.lua>\n", argv[0]);
        return 1;
    

    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushcfunction(L, l_printf);
    lua_setglobal(L, "printf");

    if (luaL_dofile(L, argv[1]) != 0) 
        fprintf(stderr, "lua: %s\n", lua_tostring(L, -1));
        lua_close(L);
        return 1;
    

    lua_close(L);

针对 Lua 5.3 进行编译,我们可以运行以下示例:

print(printf("Hello World! %d %d %d\n", 1, 5, 7) .. " bytes written")
print(printf("Hello %d %f World! %s\n", 1, 3.14, "ABC") .. " bytes written")

输出:

Hello World! 1 5 7
19 bytes written
Hello 1 3.140000 World! ABC
28 bytes written

在 SWIG 中的尝试

我想出了一个可在 SWIG 中使用的变体,但假设所有参数都可以转换为 string。这里我简单地声明printf 是一个接受十个string 类型参数的函数(如果需要更多,只需增加数量即可)。

%varargs(10, const char * = NULL) printf;
int printf(const char *fmt, ...);

这将使用 10 个字符串调用 printf 函数,这些字符串默认为空 (NULL)。因此我写了一个action,它将每个参数转换为其正确的类型(intdoublestring)。因为 SWIG 参数检查器已经在每个参数上调用 lua_tostring,所以无论实际参数类型是什么,对 lua_type 的调用总是会导致 LUA_TSTRING。这就是我使用lua_tointegerxlua_tonumberx 将字符串转换回原始类型的原因。结合基于后续转换的极其低效的失败,这为我们提供了一个类似于上面介绍的普通 C 包装器的包装器。

%module printf

%
#include <ffi.h>
%

%feature("action") printf 
    typedef union 
        int integer;
        double number;
        const char *string;
     variant;

    int argc = lua_gettop(L);
    variant *argv = malloc(argc * sizeof(variant));

    ffi_cif cif;
    ffi_type **types = malloc(argc * sizeof(ffi_type *));
    void **values = malloc(argc * sizeof(void *));

    for (int i = 0; i < argc; ++i) 
        int j = i + 1;

        int flag = 0;

        types[i] = &ffi_type_sint;
        argv[i].integer = lua_tointegerx(L, j, &flag);
        values[i] = &argv[i].integer;
        if (flag)  continue; 

        types[i] = &ffi_type_double;
        argv[i].number = lua_tonumberx(L, j, &flag);
        values[i] = &argv[i].number;
        if (flag)  continue; 

        types[i] = &ffi_type_pointer;
        argv[i].string = lua_tostring(L, j);
        values[i] = &argv[i].string;
        if (argv[i].string)  continue; 

        puts("Unhandled argment type");
        abort();
        break;
    

    // If preparing the FFI call fails we simply push -1 to indicate
    // that printf failed
    result = -1;
    if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, argc, &ffi_type_sint, types) ==
        FFI_OK) 
        ffi_call(&cif, (void (*)())printf, &result, values);
    

    free(values);
    free(types);
    free(argv);
;

%varargs(10, const char * = NULL) printf;
int printf(const char *fmt, ...);
swig -lua test.i
clang -Wall -Wextra -Wpedantic -std=c99 -I/usr/include/lua5.3 -fPIC -shared test_wrap.c -o printf.so -llua5.3 -lffi
local printf = require"printf"
printf.printf("Hello %d %f %s World!\n", 1, 3.14, "ABC")
Hello 1 3.140000 ABC World!

结束语

最后一点,这是在 Lua 中格式化字符串的一种非常低效的方法。除了printf 系列函数,我不知道 C 中的任何可变参数函数,即它们都执行字符串格式化。这在 Lua 中使用string.format 和类似的函数调用会更有效地完成

do_something("Hello %d %f %s World!\n", 1, 3.14, "ABC")

应该避免使用稍微冗长但更健壮的方式

do_something(string.format("Hello %d %f %s World!\n", 1, 3.14, "ABC"))

【讨论】:

这与我的想法相似。我想我有办法获得原始可变参数,但没有完成 FFI 位。 @Flexo 您将如何处理 SWIG 的参数检查?我想在官方文档中的 Python 示例中使用 typemap (...),但 SWIG 确定 ... 只是一个参数。 @HenriMenke 嘿,我什至不知道 Lua 中有像 string.format() 这样的东西,因为我是初学者。我认为这对于我想做的事情已经足够了。我只是想在 Lua 中构建类似于 print() 的东西,因为我无法在主机程序中使用 print() 将任何内容打印到控制台。为此,我需要向程序发送字符串。这就是我试图包装post() 的原因。我只是希望能够更轻松地打印多个变量,就像 print() 的工作原理一样。 @HenriMenke 我在写它时玩得很开心,即使我没有在这里完成其余的解决方案,它也足以作为一个自我回答发布:***.com/a/50686022/168175

以上是关于如何使用 SWIG 包装带有可变参数的 C 函数的主要内容,如果未能解决你的问题,请参考以下文章

带有 SWIG 未知长度数组的 NumPy C 扩展

带有指针的C结构,如何Swig?

SWIG:将 2d numpy 数组传递给 C 函数 f(double a[])

透明地通过带有可变参数列表的函数

将外部指针包装到 SWIG 数据结构中

用于 vsnprintf 函数统一行为的 C++ 可变参数包装器