Hello,
I've been using Nim for a little while but until now I never dig in that space of C API, at least not enough to encounter C callbacks. I want a simple thing, pass a function pointer to C code and get the C code call my proc later on. Luckily enough, the C API provides a userdata pointer to play with.
I believe there is a way Nim can do that. In Nim, a {.closure.} is represented as a function pointer plus an environment pointer. This is exactly what the C interface provides: a function pointer and a userdata. However how do I pass those to the C api and how do I ensure that the C API provides the environment pointer in the correct order with the other callback arguments? How do I ensure that there will not be a garbage collector problem (using orc)?
Surprisingly I did not find very helpful occurrences of this problem when searchin for more info.
The C API is sqlite: https://www.sqlite.org/c3ref/set_authorizer.html
bonus question: is there a way to do that if the C api does not provides a userdata pointer?
I couldn't make the rawEnv and rawProc work mostly because I don't have control over the order of parameters passed to the proc to make sure the userdata is passed around correctly.
Instead I wrapped the proc type inside a tuple that I allocate before making my C call, and I store this tuple alongside the C wrapper object so I have the guarantee that its lifetime will match the C object lifetime.
Code is here: https://github.com/mildred/easy_sqlite3/blob/ae9cde848debf023196ff9a53d82d00533b71c4c/src/easy_sqlite3/bindings.nim#L506
For the future here is the relevant piece of code:
type
SqliteRawAuthorizer* = proc(
userdata: pointer,
action_code: AuthorizerActionCode,
arg3, arg4, arg5, arg6: cstring): AuthorizerResult {.cdecl.}
Authorizer* = proc(
action_code: AuthorizerActionCode,
arg3, arg4, arg5, arg6: Option[string]): AuthorizerResult
RawAuthorizer = object
authorizer: Authorizer
type Database* = object
raw*: ptr RawDatabase
stmtcache: Table[CachedHash[string], ref Statement]
authorizer: ref RawAuthorizer
proc setAuthorizer*(db: var Database, callback: Authorizer = nil) =
let userdata: ref RawAuthorizer = new(RawAuthorizer)
userdata.authorizer = callback
proc raw_callback(
userdata: pointer,
action_code: AuthorizerActionCode,
arg3, arg4, arg5, arg6: cstring): AuthorizerResult {.cdecl.} =
let callback = cast[ref RawAuthorizer](userdata).authorizer
callback(action_code, arg3.toS(), arg4.toS(), arg5.toS(), arg6.toS())
var res: ResultCode
if callback == nil:
res = db.raw.sqlite3_set_authorizer(nil, nil)
else:
res = db.raw.sqlite3_set_authorizer(raw_callback, cast[pointer](userdata))
db.authorizer = userdata
if res != ResultCode.sr_ok:
raise newSQLiteError res
You can also just declare your proc in Nim, ensure you have {.push stackTrace:off.} around it like here: https://github.com/mratsim/trace-of-radiance/blob/e928285/trace_of_radiance/io/mp4.nim#L115-L151
proc MP4E_open(
sequential_mod_flag: cint,
enable_fragmentation: cint,
token: pointer,
write_callback: proc(
offset: int64,
buffer: pointer,
size: csize_t,
token: pointer
): cint{.cdecl, gcsafe.}
): ptr MP4E_mux_t {.minimp4, noDecl.}
## Proc coming from C that needs a callback
# Callback proc declared in Nim
{.push stackTrace: off.}
proc writeToFile(
offset: int64,
buffer: pointer,
size: csize_t,
token: pointer
): cint {.cdecl, gcsafe.} =
let file = cast[File](token)
file.setFilePos(offset)
let bytesWritten = file.writeBuffer(buffer, size)
return cint(bytesWritten.csize_t != size)
{.pop.}
proc initialize*(
self: var MP4Muxer,
file: File,
width, height: int32
) =
doAssert self.muxer.isNil, "Already initialized"
doAssert self.writer.isNil, "Already initialized"
self.muxer = MP4E_open(
sequential_mod_flag = 0,
enable_fragmentation = 0,
token = pointer(file),
write_callback = writeToFile
)
self.writer = cast[typeof self.writer](
c_malloc(csize_t sizeof(mp4_h26x_writer_t))
)
let ok = self.writer.mp4_h26x_write_init(
self.muxer,
width.cint, height.cint,
is_hevc = 0
)
doAssert ok == MP4E_STATUS_OK, "error: mp4_h26x_write_init failed"