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

Zac*_*Lee 5 c lua swig arguments

我正在尝试使用 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);
}
Run Code Online (Sandbox Code Playgroud)

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

pd.post("NUM : %d", 123);
Run Code Online (Sandbox Code Playgroud)

我收到以下错误。

Error in post expected 1..1 args, got 2
Run Code Online (Sandbox Code Playgroud)

是否可以使用 SWIG 用变量参数包装 C 函数?

我将不胜感激任何帮助。谢谢!

Hen*_*nke 3

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

链接和进一步阅读

普通 C 包装纸

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

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

printf("Hello World! %d %d %d\n", 1, 5, 7)
Run Code Online (Sandbox Code Playgroud)

将导致

Hello World! 0 0 0
Run Code Online (Sandbox Code Playgroud)

如果你幸运的话,它不会对你造成段错误,但是在像 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);
}
Run Code Online (Sandbox Code Playgroud)

针对 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")
Run Code Online (Sandbox Code Playgroud)

输出:

Hello World! 1 5 7
19 bytes written
Hello 1 3.140000 World! ABC
28 bytes written
Run Code Online (Sandbox Code Playgroud)

SWIG 中的尝试

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

%varargs(10, const char * = NULL) printf;
int printf(const char *fmt, ...);
Run Code Online (Sandbox Code Playgroud)

这将printf使用 10 个字符串调用该函数,默认情况下这些字符串为空 ( NULL)。因此我编写了一个action将每个参数转换为其正确类型(int, double, string)的方法。由于 SWIG 参数检查器已调用每个参数,因此无论实际参数类型是什么,对 的lua_tostring调用lua_type始终会导致。LUA_TSTRING这就是为什么我使用lua_tointegerxandlua_tonumberx来将字符串转换回原始类型。再加上基于成功转换的极其低效的失败,这为我们提供了一个类似于上面介绍的纯 C 包装器的包装器。

%varargs(10, const char * = NULL) printf;
int printf(const char *fmt, ...);
Run Code Online (Sandbox Code Playgroud)
%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, ...);
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
local printf = require"printf"
printf.printf("Hello %d %f %s World!\n", 1, 3.14, "ABC")
Run Code Online (Sandbox Code Playgroud)

结束语

最后一点,这是 Lua 中格式化字符串的一种极其低效的方式。除了函数系列之外,我不知道 C 中还有任何可变参数函数printf,即它们都执行字符串格式化。string.format在 Lua 中使用函数调用可以更有效地完成此操作,例如

Hello 1 3.140000 ABC World!
Run Code Online (Sandbox Code Playgroud)

应该简单地避免使用稍微冗长但更健壮的

do_something("Hello %d %f %s World!\n", 1, 3.14, "ABC")
Run Code Online (Sandbox Code Playgroud)