I want to use a C++ library in Kotlin/Native, so I...
# kotlin-native
a
I want to use a C++ library in Kotlin/Native, so I created some C wrappers for it. Cinterop correctly generates bindings, and some of them work, but some of them crash instantly. For example, I've got this C++ function that creates a new Table, and wraps it in a struct:
Copy code
/*** types.h ***/

struct JPC_Table { void *obj; };
typedef struct JPC_Table JPC_Table_t;


/*** table.cpp ***/
#ifdef __cplusplus
extern "C" {
#endif

JPC_Table_t * JPC_Table_new(unsigned long layers) {
    printf("constructor JPC_Table...\n");
    JPC_Table_t * cInstance = new JPC_Table_t();
    Table * cppInstance = new Table(layers);
    cInstance->obj = cppInstance;
    return cInstance;
};
Now, there's something wrong, but what I'd really appreciate some help with is figuring how to figure it out! I don't get any error messages, there's no stacktrace, and the
printf("...")
doesn't print anything.
the KN docs say "use the -g option on the command line", but I'm not using the command line, I'm using Gradle
l
-g
means enable debugging, you get that automatically when building the debug executable.
Can you show your
.def
file? And the Kotlin code that calls it?
a
sure! It's a little messy so it might not make sense, so I'll try to show the highlights Here's the KN main function:
Copy code
import com.jrouwe.jolt.cinterop.internal.*
import com.jrouwe.jolt.cinterop.internal.JPC_EMotionType.JPC_EMotionType_Static
import kotlinx.cinterop.*

fun main(): Unit = memScoped {
  println("starting")
  createFloor()

  println("finished")
}

@OptIn(UnsafeNumber::class)
fun MemScope.createFloor(size: Int = 50) {
  println("Creating floor, size:$size")
  val joltSettings = JPC_JoltSettings_new(null)
  JPC_JoltSettings_mMaxBodies_Set(joltSettings, 1u, null)
  println("Created settings $joltSettings")
  println("JPC_JoltSettings_mMaxBodies_Get: " + JPC_JoltSettings_mMaxBodies_Get(joltSettings, null).toString())

  val errOut = allocPointerTo<ByteVar>()

  val objectFilter = try {
    JPC_ObjectLayerPairFilterTable_new(2u, errOut.ptr)
  } finally {
    val str = errOut.value?.toKString()
    println("JPC_ObjectLayerPairFilterTable_new error $str")
  }

  println("Created object filter $objectFilter")
  // ...
}
This prints
Copy code
> Task :kayray-modules:interop-jolt2:runDebugExecutableMacosArm64 FAILED
starting
Creating floor, size:50
Created settings CPointer(raw=0x600001b040b0)
JPC_JoltSettings_mMaxBodies_Get: 1


FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':kayray-modules:interop-jolt2:runDebugExecutableMacosArm64'.
> Process 'command '/Users/dev/projects/adam/kotlin/kayray/kayray-modules/interop-jolt2/build/bin/macosArm64/debugExecutable/interop-jolt2.kexe'' finished with non-zero exit value 139
So it's this line that's failing
JPC_ObjectLayerPairFilterTable_new(2u, errOut.ptr)
And here's the function implementation
Copy code
#ifdef __cplusplus
extern "C" {
#endif

//region constructors

JPC_ObjectLayerPairFilterTable_t * JPC_ObjectLayerPairFilterTable_new(
  unsigned long inNumObjectLayers,
  char** outErrMsg
) {
  try {
    printf("constructor JPC_ObjectLayerPairFilterTable...\n");
    JPC_ObjectLayerPairFilterTable_t * cInstance = new JPC_ObjectLayerPairFilterTable_t();
    ObjectLayerPairFilterTable * cppInstance = new ObjectLayerPairFilterTable(
      inNumObjectLayers
    );
    cInstance->obj = cppInstance;
    return cInstance;
  }
  catch (exception& e) {
    if (outErrMsg) {
      *outErrMsg = strdup(e.what());
      throw e;
    }
  };
};
I really don't know why the
printf(...)
isn't working... (it's also not working in other C++ functions that otherwise succeed). I tried catching and logging any exceptions, but that doesn't seem to work either.
And the C structs that wrap the C++ classes all look like this:
Copy code
struct JPC_ObjectLayerPairFilterTable {
  void *obj;
  struct JPC_ObjectLayerPairFilter *superObj;
};
typedef struct JPC_ObjectLayerPairFilterTable JPC_ObjectLayerPairFilterTable_t;
and here's the .def file
l
This is OSX, and I'm not too familiar with that. But I think you could see such behaviour if there is a runtime linking error, so that it's never able to call the function in the first place.
On Linux I'd load it up into
gdb
and see what it says.
a
thanks for the tip, I've found that lldb is the Mac equivalent, so I'm giving it a go...
I don't think there's anything useful though...
Copy code
(lldb) r
Process 3190 launched: '[...]/build/bin/macosArm64/debugExecutable/interop-jolt2.kexe' (arm64)
starting
Creating floor, size:50
constructor JoltC_JoltSettings...
Created settings CPointer(raw=0x600000034090)
JoltC_JoltSettings_mMaxBodies_Get: 1
Process 3190 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001000033b8 interop-jolt2.kexe`kfun:dev.adamko.jolt2#createFloor__at__kotlinx.cinterop.MemScope(_this=0x0000000100218020, size=50){} at main.kt:23:16
   20  	  println("Created settings $joltSettings")
   21  	  println("JoltC_JoltSettings_mMaxBodies_Get: " + JoltC_JoltSettings_mMaxBodies_Get(joltSettings, null).toString())
   22  	
-> 23  	  val errOut = allocPointerTo<ByteVar>()
   24  	
   25  	  val objectFilter = try {
   26  	    JoltC_ObjectLayerPairFilterTable_new(2u, errOut.ptr)
Target 1: (interop-jolt2.kexe) stopped.
(lldb) thread step-over
Process 3190 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x00000001000033d0 interop-jolt2.kexe`kfun:dev.adamko.jolt2#createFloor__at__kotlinx.cinterop.MemScope(_this=0x0000000100218020, size=50){} at main.kt:26:46
   23  	  val errOut = allocPointerTo<ByteVar>()
   24  	
   25  	  val objectFilter = try {
-> 26  	    JoltC_ObjectLayerPairFilterTable_new(2u, errOut.ptr)
   27  	  } finally {
   28  	    val str = errOut.value?.toKString()
   29  	    println("JoltC_ObjectLayerPairFilterTable_new error $str")
Target 1: (interop-jolt2.kexe) stopped.
(lldb) thread step-in
Process 3190 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
    frame #0: 0x000000010000d344 interop-jolt2.kexe`kfun:kotlinx.cinterop#<get-ptr>__at__0:0(_this=0x0000600000030008){0§<kotlinx.cinterop.CPointed>}kotlinx.cinterop.CPointer<0:0> at Types.kt:174:5
Target 1: (
l
What if you just let it run (in gdb the command is just
run
). It should give you a strack trace when it crashes.
a
Copy code
(lldb) run
Process 16693 launched: '/[...]/build/bin/macosArm64/debugExecutable/interop-jolt2.kexe' (arm64)
starting
Creating floor, size:50
constructor JoltC_JoltSettings...
Created settings CPointer(raw=0x600002be80a0)
JoltC_JoltSettings_mMaxBodies_Get: 1
constructor JoltC_ObjectLayerPairFilterTable...
Process 16693 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000
error: memory read failed for 0x0
Target 2: (interop-jolt2.kexe) stopped.
l
That's a null pointer dereferencing
Can you show the stack trace? (in gdb that would be there
where
command.
Apparently it's
thread backtrace
in lldb
a
Copy code
(lldb) run
There is a running process, kill it and restart?: [Y/n] y
Process 27133 exited with status = 9 (0x00000009) killed
Process 27150 launched: '/[...]/build/bin/macosArm64/debugExecutable/interop-jolt2.kexe' (arm64)
starting
Creating floor, size:50
constructor JoltC_JoltSettings...
Created settings CPointer(raw=0x600001dbc090)
JoltC_JoltSettings_mMaxBodies_Get: 1
constructor JoltC_ObjectLayerPairFilterTable...
Process 27150 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000
error: memory read failed for 0x0
Target 2: (interop-jolt2.kexe) stopped.
(lldb) thread list
Process 27150 stopped
* thread #1: tid = 0x1fce80c, 0x0000000000000000, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  thread #2: tid = 0x1fce820, 0x000000018ecd99ec libsystem_kernel.dylib`__psynch_cvwait + 8, name = 'GC Timer thread'
  thread #3: tid = 0x1fce821, 0x000000018ecd99ec libsystem_kernel.dylib`__psynch_cvwait + 8, name = 'Main GC thread'
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  * frame #0: 0x0000000000000000
    frame #1: 0x0000000100009e68 interop-jolt2.kexe`::JoltC_ObjectLayerPairFilterTable_new(unsigned long, char **) [inlined] JPH::ObjectLayerPairFilterTable::operator new(inCount=40) at ObjectLayerPairFilterTable.h:32:2
    frame #2: 0x0000000100009e54 interop-jolt2.kexe`JoltC_ObjectLayerPairFilterTable_new(inNumObjectLayers=2, outErrMsg=0x0000600001dbc0a8) at JoltC_ObjectLayerPairFilterTable.cpp:18:48
    frame #3: 0x00000001000033f4 interop-jolt2.kexe`kfun:dev.adamko.jolt2#createFloor__at__kotlinx.cinterop.MemScope(_this=0x0000000100218020, size=50){} at main.kt:26:5
    frame #4: 0x0000000100003814 interop-jolt2.kexe`kfun:dev.adamko.jolt2#createFloor$default__at__kotlinx.cinterop.MemScope(_this=0x0000000100218020, size=0, $mask0=1){} at main.kt:16:1
    frame #5: 0x0000000100002cb8 interop-jolt2.kexe`kfun:dev.adamko.jolt2#main(){} [inlined] <inlined-out:<anonymous>> at main.kt:9:3
    frame #6: 0x0000000100002c94 interop-jolt2.kexe`kfun:dev.adamko.jolt2#main(){} [inlined] kfun:kotlinx.cinterop#memScoped(kotlin.Function1<kotlinx.cinterop.MemScope,0:0>){0§<kotlin.Any?>}0:0 at Utils.kt:703:25
    frame #7: 0x0000000100002c94 interop-jolt2.kexe`kfun:dev.adamko.jolt2#main(){} at main.kt:7:20
    frame #8: 0x0000000100003898 interop-jolt2.kexe`Konan_start(args=0x00000001001d8020) at main.kt:7:1
    frame #9: 0x0000000100005148 interop-jolt2.kexe`Init_and_run_start + 108
    frame #10: 0x000000018e98e0e0 dyld`start + 2360
(lldb)
hmmmm maybe I'm not setting the correct target when compiling with clang, and it doesn't match the target that the KN compiler uses?
I can see
constructor JoltC_ObjectLayerPairFilterTable...
is printed... it's weird that the
printf()
lines are working when using lldb but not when run via Gradle
l
It sure seems to call your code at least.
a
oh cool, lldb has a terminal ui
so I guess it's failing because of JPH_OVERRIDE_NEW_DELETE...
hmmmmmmmmmmmmmmmm
Because operator
new
for
PhysicsSystem
is overridden, you need to do:
```> auto ptr = malloc(sizeof(JPH::PhysicsSystem));
> auto phys = ::new (ptr) JPH::PhysicsSystem();
> ```
thanks so much for your help Elias! I hadn't manually debugged something like this before, so it was really useful.
Niiiiiiiice got it working! Jolt defines has some custom C++ allocators - I had to disable them by setting
JPH_DISABLE_CUSTOM_ALLOCATOR