FFI

Foreign Function Interface

S++ can interface will other language that create object or library files, and conform to the C ABI. The ffi folder must be present in the project root, and contain subfolders for each library that is being interfaced with.

For example, if the project is interfacing with the v8 library, then the ffi folder must contain a v8 subfolder. This subfolder will then contain the shared object file for the library, as well as a stub.spp file that contains the function signatures for the functions that are being called.

The ffi folder might look like:

ffi
├── v8
│   ├── libv8.so
│   ├── libv8.dylib
│   ├── libv8.dll
│   └── stub.spp
└── libuv
    ├── libuv.so
    ├── libuv.dylib
    ├── libuv.dll
    └── stub.spp

C++ Interfacing

Here are the steps involved to interface a C++ library with S++:

  1. Create the C++ library: Define the functions you want to expose to S++. They must be declared as extern "C" to prevent name mangling, so that the mapping between S++ and C++ function names is straightforward.

  2. Compile the C++ library: The target output must be a shared object file (.so on Linux, .dll on Windows, .dylib on macOS). This will combine all the relevant object files into 1 library.

  3. Make the library accessible to S++: Move the shared object into the ffi/library_name directory of your S++ project. Dependency libraries must also be moved into their respective directories. (Tool provided for this).

  4. Create a stub file: This file will contain the function signatures of the functions you want to call from S++, and the class definitions of any objects you want to create in S++. The stub file must be named stub.spp, and must reside in the ffi/library_name directory

  5. Use the functions in S++: Call the functions from S++ as if they were native functions. They will be namespaced under the standard 3rd party section, just as other vcs loaded modules would be: use ffi::library_name::*. The ffi namespace is reserved for external libraries, and the library_name namespace is the name of the shared object file.

Example (general) {collapsible=”true”}

C++ Code

// add.cpp
extern "C" __declspec(dllexport) int add(int a, int b) {
    return a + b;
}


// sub.cpp
extern "C" __declspec(dllexport) int sub(int a, int b) {
    return a - b;
}

Compile all files into a shared object file

g++ -shared -o lib_math.dll add.cpp sub.cpp
mv lib_math.dll {ProjectPath}/ffi/lib_math

S++ Stub Code

# ffi/lib_math/stub.spp
fun add(a: I32, b: I32) -> I32 { }
fun sub(a: I32, b: I32) -> I32 { }

S++ Code

use ffi::example::{add, sub};

fun main() -> Void {
    let a = add(1_u32, 2_u32)  # U32
    let b = sub(3_u32, 4_u32)  # U32
}

Conversions

As all languages available to the ffi must conform to the C ABI, it is known that all functions will have been exported using extern "C" or something similar. This means only a type-mapping between S++ and C ABI types is needed. The following table shows the conversions that are made when calling a C ABI function from S++:

C Type

S++ Type

Notes

char

I8

Identical types, no change

short

I16

Identical types, no change

int

I32

Identical types, no change

long

I64

Identical types, no change

long long

I64

Identical types, no change

unsigned char

U8

Identical types, no change

unsigned short

U16

Identical types, no change

unsigned int

U32

Identical types, no change

unsigned long

U64

Identical types, no change

unsigned long long

U64

Identical types, no change

float

F32

Identical types, no change

double

F64

Identical types, no change

long double

F64

Identical types, no change

bool

Bool

Identical types, no change

void

Void

Identical types, no change

T*

&mut T

Only as a function parameter

const T*

&T

Only as a function parameter

T[]

ArrDynamic[T]

?

T[n]

Arr[T, n]

?

Important notes

  1. The other S++ primitives (I128, I256, U128, U256, F8, F16, F128, F256) do not have direct C equivalents, and so cannot be used in FFI calls.

  2. Not all features of every language will be supported. For example, C++ templates are not supported, and so cannot be used in FFI calls, until a template <-> generics conversion is implemented.

  3. Other languages classes and constructors aren’t supported by S++ as object initialization is done in S++ itself. Again, as the system is further developed, this may change.

  4. C++ functions returning pointers cannot be interfaced into S++, as S++ does not allow borrows as return values, enforcing second class borrows. Instead, the smart pointer Single[T] is returned, as this represents a value on the heap.

Safety

S++ does not have a concept of pointers, and so cannot directly access memory. This means that the FFI is inherently safe, as it cannot cause memory corruption or leaks. However, the C++ functions can still cause undefined behaviour if they are not written correctly.

As such, all FFI code, no matter the language, is executed in isolated stacks, with every function call’s return value being wrapped in the result Res[Pass, Fail] type. This means that if the function call fails, then the error will be propagated up the call stack, and the program will not crash.

Exporting S++ Functions

S++ functions can be exported to other languages, but only if the functions are public and follow the C ABI. This is done by using the @public and @repr_c annotation, (with the argument being the “C” convention):

@public
@repr_c
fun add(a: I32, b: I32) -> I32 {
    ret a + b
}

This encourages a design where a separate @repr_c API is created, which calls the internal S++ functions. This allows the internal functions to be changed without affecting the external API, and also makes the API safer by isolating the FFI code from the rest of the program.

Design Decisions

  1. The reason for having subfolders within the ffi folder is that shared objects for different platforms can be placed in the same folder, and the correct one will be chosen during compilation. It also assists with dependency management.