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
:
- Generate a regular Scala trait
libtest.ExportedFunctions
which will contain all the functions from the header file - Generate a
libtest.functions
object that extendslibtest.ExportedFunctions
, where each function is given a body, which invokeslibtest.impl.Implementations.<funcName>
- wherelibtest.impl.Implementations
also extendslibtest.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.