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"