https://kotlinlang.org logo
Title
a

Adam S

04/27/2023, 6:07 PM
I’m calling a cinterop-generated function that requires a callback as an argument. I’m using
staticCFunction()
to pass in a custom callback, but the callback requires some boilerplate to handle the C types. Is it possible to ‘wrap’ this callback with one of my own, without breaking the ‘no references’ rule for
staticCFunction()
? What I want is something like this, but this fails because “kotlinx.cinterop.staticCFunction must take an unbound, non-capturing function or lambda, but captures at (…) callback”
fun main() {
  attachAudioMixedProcessor { samples, frames ->
    for (frame in 0 until frames.convert()) {
      val left = samples[frame * 2 + 0]
      val right = samples[frame * 2 + 1]

      // ...
    }
  }
}

fun attachAudioMixedProcessor(
  callback: (samples: CPointer<FloatVar>, frames: Int) -> Unit
) {
  // call the c-interop function
  AttachAudioMixedProcessor(staticCFunction { buffer: COpaquePointer?, frames: UInt ->
    val samples: CPointer<FloatVar> = buffer?.reinterpret() ?: return@staticCFunction
    callback(samples, frames.toInt())
  })
}

// generated C interop function
external fun AttachAudioMixedProcessor(processor: CPointer<CFunction<(COpaquePointer?, UInt) -> Unit>>?)
what I have:
fun main() {
  // cinterop function
  AttachAudioMixedProcessor(staticCFunction(::processAudio))
}

/** Audio processing function */
private fun processAudio(buffer: COpaquePointer?, frames: UInt) {
  val samples: CPointer<FloatVar> = buffer?.reinterpret() ?: return

  for (frame in 0 until frames.convert()) {
    val left = samples[frame * 2 + 0]
    val right = samples[frame * 2 + 1]

    // ...
  }
}
v

vbsteven

04/27/2023, 6:37 PM
Are you in control of the original C function? or is it a function from a library? Many C libraries that are callback-based have some sort of provision for setting a
user_data
pointer when calling or configuring the callback, and then when calling the callback they will pass the original
user_data
as one of the arguments. This way when the callback function is called, you have a way of getting the context back from inside the "standalone" callback.
a

Adam S

04/27/2023, 6:38 PM
it’s the function of a library, here’s the header definitions:
void AttachAudioMixedProcessor(AudioCallback processor);
typedef void (*AudioCallback)(void *bufferData, unsigned int frames);
v

vbsteven

04/27/2023, 6:47 PM
If you cannot pass any
user_data
I think your only option is to have some other outside
Object
or
fun
that you can access from inside the callback function.
a

Adam S

04/27/2023, 6:49 PM
those would have to be top-level objects or functions, right?
v

vbsteven

04/27/2023, 6:50 PM
yes indeed, AFAIK they are not capturing and can be called from within staticCFunction
And it shouldn't be a problem if you only have one of these and you don't need to distinguish between multiple objects on the Kotlin side that might re-use the same callback
a

Adam S

04/27/2023, 6:51 PM
hmm, I think I see what you mean but I don’t think it really helps avoiding the boilerplate
v

vbsteven

04/27/2023, 6:52 PM
from your first code sample
you can store the
callback
value inside an
object
, and in the staticCFunction, get it back from the
object
a

Adam S

04/27/2023, 6:54 PM
oh I see! Hmm….
there’s multiple callbacks (the example just showed one) so I’d have to try and be clever…
v

vbsteven

04/27/2023, 6:56 PM
yes indeed, that is the reason why lots of C callback-based libraries have that
user_data
pointer, so you have something to distinguish for who the callback was intended
a

Adam S

04/27/2023, 7:15 PM
ah, so they can put some sort of ID in the data and use that to fetch it from the global-static list?
v

vbsteven

04/27/2023, 7:16 PM
indeed, and it doesn't even need to be a global-static list
it's just a pointer that is passed in both when registering the callback function, and as an argument when the callback function is invoked
so for my GTK work on gtk-kn I've used this a lot, and I typically use a
StableRef
to the Kotlin lambda when registering the callback. And in the staticCFunction I can extract the original Kotlin callback again from the
StableRef
using the user_data pointer, and then I can invoke the Kotlin callback from within
staticCFunction
a

Adam S

04/28/2023, 11:15 AM
I got it working! Thanks for your help and tips @vbsteven
v

vbsteven

04/28/2023, 11:15 AM
Nice, how did you end up doing this?
a

Adam S

04/28/2023, 11:15 AM
haha I was afraid someone would ask that - it’s not easy to summarise
I have a file that contains the package-level function which is used as a callback, and list of processors, which are iterated over in the callback
AudioProcessor
is a
fun interface
that accepts the converted arguments
and then in my main function I can register multiple processors, either as lambdas or function references
attachAudioMixedProcessor(::processAudio)
v

vbsteven

04/28/2023, 11:22 AM
That makes sense, however you are attaching and detaching both time using a
staticCFunction
, I'm not sure if both of these invocations will resolve to the same pointer, so it could be that you are detaching a different function than the one you are attaching
when I use
staticCFunction
I usually define it once as a
val
in the file, and then reference that.
private val myCFunc = staticCFunction {
  // do the processing
}
a

Adam S

04/28/2023, 11:24 AM
ahh okay, I assumed I could only call
staticCFunction {}
when all the processors were added, and the function was ‘ready’
the library doesn’t seem to mind if the attached/detached functions are different - I think that’s there so you can dynamically add/remove processors, which I don’t currently need
v

vbsteven

04/28/2023, 11:27 AM
AFAIK using
staticCFunction
will make sure there is a C function defined that can be called from C code. It might be smart enough to resolve to the same pointer if the same kotlin function is passed in, but I'm not sure
a

Adam S

04/28/2023, 11:29 AM
this works well 👍
// must be a static singleton, required by `staticCFunction {}`
internal object AudioDeviceInitializerImpl : AudioDeviceInitializer {
  override fun attachAudioMixedProcessor(processor: AudioProcessor) {
    mixedAudioProcessors += processor
  }

  internal fun attach(): Unit = AttachAudioMixedProcessor(processAllStatic)
  internal fun detach(): Unit = DetachAudioMixedProcessor(processAllStatic)
}

// must be a package-level function, required by `staticCFunction {}`
private val mixedAudioProcessors = mutableListOf<AudioProcessor>()

// must be a package-level function, required by `staticCFunction {}`
private fun processAll(buffer: COpaquePointer?, frames: UInt) {  for (
processor in mixedAudioProcessors) {
    // ...
  }
}

/** static reference to [processAll] */
private val processAllStatic = staticCFunction(::processAll)