FFI with C
The Nit Foreign Function Interface (FFI) allows to nest C code within a Nit file. Doing so, you can implement Nit methods in C, declare the C type equivalent to a Nit class, and write supporting C code (such as imports). This page is an introduction to different aspects of the Nit FFI.
Common use cases of the FFI is to optimize a method or wrap existing C libraries. In case of a wrapper, the C code manages calls to the C library functions and callbacks to Nit. To know more about wrappers using the FFI, see the page Wrapping C Libraries.
Extern methods
An extern method is a Nit method implemented with C code. Except for its implementation, it behaves like any other Nit method. It has a receiver, can refine or specialize an existing property, be refined itself and even have virtual types in its signature.
Extern methods are declared and implemented like so in the Nit code:
class A fun foo `{ printf( "in method A::foo, from Cn" ); `} fun bar( n : Char ) : Bool `{ printf( "in method A::bar( %d ), from Cn", n ); return 1; `} end
Notice that the body of the previous methods are implemented in C. This code behaves as a C function with a hidden signature. For the previous methods, the hidden signatures are:
void A_foo___impl( A recv ) int A_bar___impl( A recv, char n )
The Nit types considered as primitive to C are converted in the C signature (as for Char and Bool) whereas Nit types unknown to C are kept opaque (as for the receiver of type A).
Primitive Nit types in C
For easier passage between Nit and C, Nit types that are primitive to C are converted automatically. This conversion works both ways.
Primitive types between Nit and C
Nit type | C type | Notes |
---|---|---|
Int |
long |
The normal primitive type in C |
Bool |
int |
Where 0 is false and everything else true |
Float |
double |
|
Char |
uint32_t |
|
Byte |
unsigned char |
|
NativeString |
char * |
The standard C string |
Pointer |
void * |
A general pointer |
Note on NativeString
NativeString
is a pointer, it may point to memory managed by the garbage collector.
However, apply special care when dealing with NativeString
instances from C,
especially to avoid memory leaks or painful-to-debug segmentation faults.
To avoid any problems when dealing with NativeString
, follow these basic rules:
- If you don't know the origin of a
NativeString
, it may have been allocated by the GC. - Be particularly careful when modifying a
NativeString
as they might be shared by several instances, a general rule would be not to modify only copies of the receivedchar*
. - When returning an extern resource, allocate a
NativeString
managed by the GC throughnew_NativeString
in C or useNativeString::to_s_with_copy
.
Nit types in C
All Nit types that are not primitive to C are represented by an opaque C type. The opaque C type is generated when needed with a name corresponding to the Nit type. Usually the C type will have the same name as its Nit equivalent, with the exception of nullable types (in which the type name is prefixed with nullable_) and generic types (see examples below).
Some equivalent types between Nit and C
Nit type | C type | Notes |
---|---|---|
Object |
Object |
A generated opaque type |
String |
String |
A generated opaque type
(NativeString is more practical in C) |
nullable Object |
nullable_Object |
Using casts (discussed later on this page)
it can be cast to Object |
Array[String] |
Array_of_String |
There is a _of_ between the
class name and it’s parameter. |
Map[Int, String] |
Map_of_Int_String |
The parameters are separated only with a
_ |
nullable Array[String] |
nullable_Array_of_String |
These can get quite long, but they are precise! |
Callbacks to Nit from C
As seen previously, to invoke C code from Nit code, you must use extern methods. But when calling Nit code from C, you must use the callback system of the Nit FFI.
Any callbacks from C to Nit must be declared after the signature of the extern method. In fact, they must be declared on the last Nit extern method on the stack. With this information, the compiler will generate a custom C function implementing the callback and will make it available to the C code implementing the last Nit extern method.
The generated functions are named using the receiver type and the method name.
class A var my_attr = 1234 fun baz( msg : String ) import String.length, String.to_cstring, my_attr, my_attr `{ char *c_msg; int msg_len; /* String_to_cstring is a callback to msg.to_cstring */ c_msg = String_to_cstring( msg ); /* String_length is a callback to msg.length */ msg_len = String_length( msg ); printf( "received msg: %s, of length = %dn", c_msg, msg_len ); /* A_my_attr is a callback to the getter of self.my_attr */ printf( "old attr %dn", A_my_attr(recv) ); /* A_my_attr is a callback to the setter of self.my_attr */ A_my_attr__assign( recv, msg_len ); `} end
As you may have noticed, the callback functions in C use a similar name to the equivalent Nit method and the implementation functions. This pattern can be used to predict the name of the C function to called a given Nit method.
It is also possible to callback the Nit language for more than methods:
- Declaring an import of
super
in Nit will allow to call super from C using a function similar toA_baz___super( recv )
. - Declaring an import a constructor will allow it to be called from C as
A new_A()
for the anonymous constructor of the class A orA new_A_name()
for a named constructor. - Casts can de declared as an import in Nit with three forms;
A as(B)
,String as nullable
,String as not nullable
. The first form is used to cast from any to any type. The second and third forms are used to cast between nullables and non-nullables. In the C code, those imports allows to use two set of funtions, one for the type check and one for the cast itself. With these examples we would getint A_is_a_B( A )
andB A_as_B( A )
for the first form;nullable_String String_as_nullable( String )
for the second form (no check is necessary) as well asint String_is_null( nullable_String )
andString String_as_not_null
for the third form.
Callback Formats by Examples
We will use examples in order to familiarize you to the structure generated callback functions. But first, we setup the context in the next code segment. Then, we explain the callbacks from my_c_meth
.
Example Context
class MyClass var attr: Object var attr_int: Int init(obj: Object, val: Int) do self.attr = obj self.attr_int = val end init default do self.attr = "Hello" self.attr_int = 8 end fun foo do print "foo" fun bar(text: String, val: Int): Bool do return text.is_numeric and text.to_i == val fun my_c_meth import foo, bar, attr, attr, attr_int, attr_int, MyClass, MyClass.default, NativeString.to_s `{ // C implementation code `} end
Explanation of each Callbacks
Importation in Nit | C Function Signature | Note |
---|---|---|
foo |
void MyClass_foo(MyClass) |
The single argument is the receiver |
bar |
int MyClass_bar(MyClass, String, long) |
One receiver, two parameters and a return. |
attr |
Object MyClass_attr(MyClass) |
Getter of attribute attr |
attr= |
void MyClass_attr__assign(MyClass, Object) |
Setter of attribute attr |
attr_int |
long MyClass_attr_int(MyClass) |
Getter of attribute attr |
attr_int= |
void MyClass_attr_int__assign(MyClass, long) |
Setter of attribute
attr_int |
MyClass |
MyClass new_MyClass() |
A call to the anonymous constructor |
MyClass.default |
MyClass new_MyClass_default() |
A call to the constructor
default |
NativeString.to_s |
String NativeString_to_s(char*) |
Converts the primitive type
char* to the Nit class String |
String.to_cstring |
char* String_to_cstring(String) |
Converts the primitive type
char* to the Nit class String |
Int.+ |
long Int__plus(long, long) |
Operators such as + use a
literal name prefixed by _ |
Container[Int].item= |
void Container_of_Int_item__assign(Container_of_Int, long) |
Add to a generic array bounded by String |
Array[String].add |
void Array_of_String_add(Array_of_String, String) |
Add to a generic array bounded by String |
Array[String].[] |
String Array_of_String__index(Array_of_String, long) |
Get from an array. |
Array[String].[]= |
void Array_of_String__index_assign(Array_of_String, long, String) |
Insert into an array. |
HashMap[String, Array[String]].[] |
Array_of_String HashMap_of_String_Array_of_String__index (HashMap_of_String_Array_of_String, String) |
When the types are too complex like this one, you are better of to write the complex code in Nit. |
Extern classes
A Nit extern class is a special kind of class that are associated to a custom C type. An instance of this class can then be used like other primitives when passed to C where its equivalent type will be used.
An extern class is declared in Nit using the extern
keyword before class
. The C type equivalent is defined in C right after the class name.
# A Nit extern class associated to the C type `my_struct *` extern class MyStruct `{ my_struct* `} # ... end
The single value of an instance of an extern class is its value in C, which means that it has no attributes. An extern Nit classes still can have methods (normal and extern) and constructors (but only extern constructors, presented shortly).
In total, extern classes impose 3 restrictions:
- The extern class cannot define attributes.
- The equivalent C type must be a pointer (it is actually a subclass of
Pointer
in Nit) - The extern class can only subclass other extern classes and interfaces.
Instances of extern classes can originate from the C code, or from an extern constructors. Extern constructors are declared using the new keyword and are implemented similarly to extern methods. They must return a value in the equivalent C type.
extern class MyStruct `{ my_struct* `} # Extern anonymous constructor doing a simple malloc new `{ return malloc( sizeof( my_struct) ); `} # Named extern constructor a bit more complexe new named `{ A a = malloc( sizeof( my_struct) ); (*a).age = 21; // this wiki do not support the standard arrow return a; `} end
You must free any resources you allocate in C code, the Nit system do not manage these.
Examples of extern classes types in Nit and C
Nit type | C type | Notes |
---|---|---|
MyStruct |
my_struct * |
As declared by the user (in the example above) |
NativeFile |
FILE * |
A handle to a file, from the standard library |
FileStat |
struct stat * |
Informations on a file, from the standard library |
Global C references
Nit objects passed as argument to a C implementation function are guaranteed to be valid until the function return. So it is for Nit objects returned from callbacks to Nit code. However, if you need to keep a Nit object between functions, in a global C variable, you have to notify the native interface for it to sta valid.
For example, before saving an instance of the class A in a global variable, you must call A_incr_ref( A )
on the instance. From then on, the Nit system will update this reference at each garbage collection so it stays valid, even after the current extern method returns. To free such a reference, call A_derc_ref( A )
.
Compiling and linking
Compiling a Nit program using the basic FFI is straight forward:
nitc my_module.nit
When using a shared C library you will have to specify more options for the compiler.
Passing options to the C compiler
For advanced use of the FFI, you'll often need to pass custom options to the C compiler. The most common are probably: -l
to link with a shared library and -I
to specify where to find C header files.
The Nit FFI offers 3 annotations to use on the modules nesting C code: c_compiler_options
, c_linker_options
and pkgconfig
.
pkgconfig
The program pkg-config
knows what options are needed to compile a specific with popular C libraries. The annotation pkgconfig
call the program in the background to get the options.
Usage examples:
module glesv2 is pkgconfig # Will use the options for the name module `pkg-config glesv2`
or
module gtk3_4 is pkgconfig("gtk-3.0")
cflags and ldflags
When you need specialized options or a library unsupported by pkg-config, you can use the annotations cflags
and ldflags
. They pass options directly the compiler and linker.
They can be used with a special function call to exec
which will use the result of a local program instead of a string.
Basic usage example:
module sdl is cflags("-I", "/usrincludeSDL") ldflags("-l", "SDL") end
Same example with calls to exec
, where we use the external program sdl-config
:
module sdl is cflags(exec("sdl-config", "--cflags")) ldflags(exec("sdl-config", "--libs"), "-lSDL_image -lSDL_ttf") end