Today a post about the
difficulties with some pointer-related types in the current implementation of OBS Lua scripting. No trick, no tip.
As mentioned in my previous post (where I attached the C wrapper file), scripting in OBS is based on SWIG. It makes a very good job at building bindings based on the C code, but some C constructs just cannot be converted properly due to the inherent ambiguity of
C pointers. i.e. because of pointer/array equivalence or arguments passed by reference. In some cases, SWIG would provide mechanisms to cope with that.
Let's look at some examples and try to categorize by data type.
Type char *
The type
char *
is commonly used to manipulate character strings like in
strcpy
(please note that the standard C does not bother too much with "const"). The type
char *
has several interpretations when passed as argument to a function:
- Memory area where the function can write some character-typed data
- Memory area containing a zero-ended sequence of characters (a classical C character string) to be read by the function. Normally it should be typed
const char *
or char const *
(both are equivalent).
- Pointer to a single character that can be changed by the function, or not (again a
const
would be expected if read-only)
- Array of characters, where a zero has no special meaning (in this case the type should be
char[]
, or const char[]
if it is read-only)
Obviously, depending on this interpretation, the C code would be different and the type used in Lua should reflect it, but S
WIG interprets any char *
or const char *
as a Lua string. It works smoothly in most cases.
In the function seen previously
int os_get_config_path(char *dst, size_t size, const char *name)
(used internally by OBS to retrieve e.g. Roaming AppData on Windows, thanks WizardCM), the destination
dst
is a pointer on a memory block where the function will copy the resulting string content. In other words it is a character string passed by reference, which is not supported in Lua.
This violates the immutability of Lua strings: the data of the string is replaced at its current memory location, i.e. without new allocation. The size of the allocated memory area can be passed in the
size
argument, but obviously the string passed to the function needs to contain already a number of characters at least equal to
size
, such that enough space is allocated to prevent a buffer overflow.
What could be a better binding for this function? There is no simple answer because there is just no concept of passing arguments by reference in Lua. Actually, SWIG foresees a smart Lua-style (or Python-style) solution:
redefining the arguments as OUTPUT, i.e. the variables passed by reference are not modified in place but multiple values are returned.
Another example is the function
char *os_generate_formatted_filename(const char *extension, bool space, const char *format)
. It
returns a new bmalloc-allocated filename generated from specific formatting.
In the corresponding wrapper code, the memory is allocated by the function, then the content is copied in a new Lua-controlled memory block via
lua_pushstring, but at the end
the allocated memory block is systematically leaked:
C:
static int _wrap_os_generate_formatted_filename(lua_State* L) {
[...]
result = (char *)os_generate_formatted_filename((char const *)arg1,arg2,(char const *)arg3);
lua_pushstring(L,(const char *)result); SWIG_arg++;
return SWIG_arg;
[...]
Actually SWIG provides a
feature called "%newobject" to cope with memory allocation within a function. Slowly I'm thinking again about a fix for OBS scripting (but I tried a bit and I know it is not so easy to put in place given the number of functions in OBS).
Types uint8_t * and uint32_t *
The situation is different with pointers on data buffers. They can be found in a number of functions and getters/setters (no occurrence of
uint16_t *
so far).
First example, for the undocumented member
texture_data
of
gs_image_file
, SWIG creates a "set" function:
C:
static int _wrap_gs_image_file_texture_data_set(lua_State* L) {
[...]
if(!SWIG_isptrtype(L,2)) SWIG_fail_arg("gs_image_file::texture_data",2,"uint8_t *");
[...]
if (!SWIG_IsOK(SWIG_ConvertPtr(L,2,(void**)&arg2,SWIGTYPE_p_unsigned_char,SWIG_POINTER_DISOWN))){
SWIG_fail_ptr("gs_image_file_texture_data_set",2,SWIGTYPE_p_unsigned_char);
}
if (arg1) (arg1)->texture_data = arg2;
[...]
And a "get" function:
C:
static int _wrap_gs_image_file_texture_data_get(lua_State* L) {
[...]
uint8_t *result = 0 ;
[...]
result = (uint8_t *) ((arg1)->texture_data);
SWIG_NewPointerObj(L,result,SWIGTYPE_p_unsigned_char,0); SWIG_arg++;
return SWIG_arg;
[...]
SWIG manages the data internally as
SWIGTYPE_p_unsigned_char
(not as a Lua string) and uses various
pointer conversion functions . The big question is now:
how to transfer data between Lua and this buffer? I have no answer so far.
One promising alternative is the very low-level
ffi library. It can allocate a buffer of particular type, i.e. like in the following code:
Lua:
ffi = require("ffi")
obslua.obs_enter_graphics()
local image = obslua.gs_image_file()
image.cx = 1
image.cy = 1
image.texture_data = ffi.new("uint8_t[?]", 4)
Unfortunately this code does not work. The allocated buffer is a "cdata" type, not userdata.
The execution stops with this error:
Error running file: Error in gs_image_file::texture_data (arg 2), expected 'uint8_t *' got 'cdata'
It seems that there is
no foreseen way to convert cdata into userdata. Now crafting userdata from Lua seems to be possible with the
crazy pure-Lua "luastate" library. It is able to re-create the complete binary object used in Lua bindings. This would be the ultimate hack for all problems! But way too complex to use I think.
Reading data from a buffer seems to be supported by
ffi.string(ptr [,len] )
. The following code can be executed without error (
hex_dump
is a function to display bytes from a string):
Lua:
ffi = require("ffi")
local url = encode_bitmap_as_URL(3, 2, {0xFF0000FF, 0xFFFFFFFF, 0xFFFF0000, 0x7F0000FF, 0x7FFFFFFF, 0x7FFF0000})
obslua.obs_enter_graphics()
local image = obslua.gs_image_file()
obslua.gs_image_file_init(image, url)
local str = ffi.string(image.texture_data, image.cx*image.cy*4)
if str then print("Image data as string: " .. hex_dump(str)) end
obslua.obs_leave_graphics()
But again, unfortunately, this is no solution. For a reason I cannot follow, the returned string does not contain the expected data:
Image data as string: 08 f2 d1 54 fc 7f 00 00 00 00 00 00 00 00 00 00 00 5a 8d 95 99 02 00 00
There is maybe still something to be converted (e.g. getting a pointer on the buffer referenced by the userdata), I don't see how.
Another example with the function
void gs_get_size(uint32_t *cx, uint32_t *cy)
and this wrapper:
C:
static int _wrap_gs_get_size(lua_State* L) {
[...]
if (!SWIG_IsOK(SWIG_ConvertPtr(L,1,(void**)&arg1,SWIGTYPE_p_unsigned_int,0))){
SWIG_fail_ptr("gs_get_size",1,SWIGTYPE_p_unsigned_int);
}
if (!SWIG_IsOK(SWIG_ConvertPtr(L,2,(void**)&arg2,SWIGTYPE_p_unsigned_int,0))){
SWIG_fail_ptr("gs_get_size",2,SWIGTYPE_p_unsigned_int);
}
gs_get_size(arg1,arg2);
return SWIG_arg;
[...]
}
No need to try, the pointers passed as reference are interpreted as inputs, and the result of the function is lost.
Pointer-pointer types
This was my main concern as I started to explore Lua bindings, and the reason I wrote an
issue on GitHub.
With the function
bool gs_texture_map(gs_texture_t *tex, uint8_t **ptr, uint32_t *linesize)
, as we saw previously, there is no direct way to create a userdata with type
uint8_t **
. Even if some variable or pointer could be passed, the result would be lost.
The function
gs_effect_t *gs_effect_create(const char *effect_string, const char *filename, char **error_string)
is a bit more interesting as it deals with a character string. The wrapper includes:
C:
if (!SWIG_IsOK(SWIG_ConvertPtr(L,2,(void**)&arg2,SWIGTYPE_p_p_char,0))){
SWIG_fail_ptr("gs_effect_create_from_file",2,SWIGTYPE_p_p_char);
}
The pointer-pointer is not converted into a string so it is interpreted as input only.
Conclusion
This post was frustrating to write! This is a sequence of dead-ends.
Fortunately, most of the functions in the OBS API are usable and with some workarounds (e.g. the BMP loading) I did not identify real blocking points so far. But someone may want to read a picture file and retrieve its RGBA data or get the errors from an effect file compilation. In my opinion there is no possibility to use the related functions in the OBS API for that.
I see two approaches still to explore now:
- Adapt the SWIG binding rules, risking to break existing Lua plugins
- Explore the possibilities of the FFI library to call directly the C functions without going through the SWIG bindings. That could work.
That's all for today.