SN bindgen

Scala 3 Native binding generator to C libraries

Exporting binary interface to Scala Native code

You can use bindgen not only to interface with C libraries, but also to define the C-compatible interface of a Scala Native project built as a dynamic or shared library.

Scala Native itself supports it via @exported family of annotations.

Say you want to expose your Scala Native code as a dynamic/shared library. You could manually define your functions like this:

import scalanative.unsafe.*

@exported
def MYLIB_func1(): CString = c"hello!"

@exported
def MYLIB_func2(i: Int, b: Long): Long = i * b

And for a simple interface with primitive types that's exactly what we recommend doing.

But what if your functions are more complex? They could involve structs, enums, and more complex C types than just primitives. Especially if your target runtime supports complex C types - like Swift, for example, that can work directly with header files and handle lots of different C types.

In this case, Bindgen supports a special export mode, which will do the following, assuming you generate bindings in package libtest:

  1. Generate a regular Scala trait libtest.ExportedFunctions which will contain all the functions from the header file
  2. Generate a libtest.functions object that extends libtest.ExportedFunctions, where each function is given a body, which invokes libtest.impl.Implementations.<funcName> - where libtest.impl.Implementations also extends libtest.ExportedFunctions - that's where you can define implementations for your functions.
  • In CLI, this mode can be activated by using the --export flag

  • In SBT, you can enable it by using .withExport(true) on the Binding builder: Binding.builder(<headerFile>, <packageName>).withExport(true).build

If this sounds confusing, let's take a look at a very simple example, where the interface doesn't use anything other than primitive types:

Source C code

long myThing(int n, long i);

Generated Scala code

package libtest

import _root_.scala.scalanative.unsafe.*
import _root_.scala.scalanative.unsigned.*
import _root_.scala.scalanative.libc.*
import _root_.scala.scalanative.*

trait ExportedFunctions:
  /**
   * [bindgen] header: /tmp/1844071101488525804header.h
  */
  def myThing(n : CInt, i : CLongInt): CLongInt


object functions extends ExportedFunctions:
  /**
   * [bindgen] header: /tmp/1844071101488525804header.h
  */
  @exported
  override def myThing(n : CInt, i : CLongInt): CLongInt = libtest.impl.Implementations.myThing(n, i)



If you attempt to compile the generated Scala code as is, it won't work - because you need to provide implementations.

The reason implementations are expected in a separate file is to allow you to edit the header file (which defines your binary interface) separately from implementations. And the ExportedFunctions trait exists to assist in defining said implementations - making it IDE friendly.

Here's an example of how you can define Implementations:

package libtest.impl 

import libtest.all.*

object Implementations extends libtest.ExportedFunctions:
  override def myThing(n: Int, i: Long): Long = 
    n * i

Now that you saw this trivial example, here's a more complex one, which uses structs. Usual restrictions still apply - structs have to be received by address (param: Ptr[MyStruct]), not by value (param: MyStruct):

Source C code

typedef struct {
  int length;
  const char *str;
} MyStuff;

long myThing(int n, const MyStuff* str);

Generated Scala code

package libtest

import _root_.scala.scalanative.unsafe.*
import _root_.scala.scalanative.unsigned.*
import _root_.scala.scalanative.libc.*
import _root_.scala.scalanative.*

object structs:
  import _root_.libtest.structs.*
  /**
   * [bindgen] header: /tmp/15061531050369736124header.h
  */
  opaque type MyStuff = CStruct2[CInt, CString]
  object MyStuff:
    given _tag: Tag[MyStuff] = Tag.materializeCStruct2Tag[CInt, CString]
    def apply()(using Zone): Ptr[MyStuff] = scala.scalanative.unsafe.alloc[MyStuff](1)
    def apply(length : CInt, str : CString)(using Zone): Ptr[MyStuff] = 
      val ____ptr = apply()
      (!____ptr).length = length
      (!____ptr).str = str
      ____ptr
    extension (struct: MyStuff)
      def length : CInt = struct._1
      def length_=(value: CInt): Unit = !struct.at1 = value
      def str : CString = struct._2
      def str_=(value: CString): Unit = !struct.at2 = value

trait ExportedFunctions:
  import _root_.libtest.structs.*
  /**
   * [bindgen] header: /tmp/15061531050369736124header.h
  */
  def myThing(n : CInt, str : Ptr[MyStuff]): CLongInt


object functions extends ExportedFunctions:
  import _root_.libtest.structs.*
  /**
   * [bindgen] header: /tmp/15061531050369736124header.h
  */
  @exported
  override def myThing(n : CInt, str : Ptr[MyStuff]): CLongInt = libtest.impl.Implementations.myThing(n, str)

object types:
  export _root_.libtest.structs.*

object all:
  export _root_.libtest.structs.MyStuff


On static libraries and ScalaNativeInit

If you read the section about native exports, you can see a reminder to call ScalaNativeInit() function to initialise the Scala Native GC runtime.

Therefore if you are interacting with an ecosystem that handles C header files natively (like Swift), it's convenient to put ScalaNativeInit into the header file that defines your binary interface.

Bindgen recognises that and doesn't render this function as part of the bindings. The function has to exactly match the int ScalaNativeInit(void) type to be filtered out.