Disclaimer: Not really an answer, because I didn't find a way to override SWIG's argument checking, so I could process varargs myself. This can be solved by combining the methods I show below with this answer.
Links and further reading
Plain C wrapper
I prepared an example how to convert a call to a variadic Lua function to a variadic C function using libffi (documentation).
Currently the code only handles int
(requires Lua 5.3), double
and const char *
arguments. It can be trivially extended to more types as well. Keep in mind that this approach is extremely unsafe. Using an unsupported format will result in a segmentation fault (the format string goes unchecked). For example compiling against Lua 5.2 and trying to use the integer format like so
printf("Hello World! %d %d %d\n", 1, 5, 7)
will result in
Hello World! 0 0 0
if you are lucky and it doesn't segfault on you, but running the program in a memory debugger like valgrind will reveal that you are doing nasty things.
// 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);
}
Compiling against Lua 5.3 we can run the following example:
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")
Output:
Hello World! 1 5 7
19 bytes written
Hello 1 3.140000 World! ABC
28 bytes written
An attempt in SWIG
I have come up with a variant which is usable from within SWIG, but makes the assumption that all arguments are convertible to string
. Here I simply declare printf
to be a function taking ten arguments of type string
(if you need more, just increase the number).
%varargs(10, const char * = NULL) printf;
int printf(const char *fmt, ...);
This would call the printf
function with 10 strings, which are by default empty (NULL
). Therefore I wrote an action
which converts each argument to its proper type (int
, double
, string
). Because the SWIG argument checker calls lua_tostring
on each argument already, a call to lua_type
would just always result in LUA_TSTRING
, no matter what the actual argument type was. That is why I'm using lua_tointegerx
and lua_tonumberx
, to convert from the string back to the original type. Combined with an extremely inefficient fallthrough based on succeeding conversion, this gives us a wrapper similar to the plain C wrapper presented above.
%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!
Closing remarks
As a final note, this is a horribly inefficient way of formatting strings in Lua. I am not aware of any variadic function in C other than the printf
family of functions, i.e. all of them perform string formatting. This is done much more efficiently in Lua using string.format
and a function call like
do_something("Hello %d %f %s World!\n", 1, 3.14, "ABC")
should simply be avoided in favour of the slightly more verbose, but much more robust
do_something(string.format("Hello %d %f %s World!\n", 1, 3.14, "ABC"))