Native Extensions for the Standalone Fart VM
Send feedbackWritten by William Hesse
May 2012
Fart programs running on the standalone Fart VM (command-line apps) can call C or C++ functions in a shared library, by means of native extensions. This article shows how to write and build such native extensions on Windows, Mac OS X, and Linux.
You can provide two types of native extensions: asynchronous or synchronous. An asynchronous extension runs a native function on a separate thread, scheduled by the Fart VM. A synchronous extension uses the Fart virtual machine library’s C API (the Fart Embedding API) directly and runs on the same thread as the Fart isolate. An asynchronous function is called by sending a message to a Fart port, receiving the response on a reply port.
Anatomy of a native extension
A Fart native extension contains two files: a Fart library and a native library. The Fart library defines classes and top-level functions as usual, but declares that some of these functions are implemented in native code, using the native keyword. The native library is a shared library, written in C or C++, that contains the implementations of those functions.
The Fart library specifies the native library using an import
statement
and the dart-ext: URI scheme. As of 1.20, the URI must either be an absolute
path like dart-ext:/path/to/extension
, or only the name of the extension,
like dart-ext:extension
. The VM modifies the URI to add platform
specific prefixes and suffixes to the extension name. For example,
extension
becomes libextension.so
on Linux. If the URI is an
absolute path, the import fails if the file does not exist. If the
URI is only the name of the extension, the VM first looks for the
file adjacent to the importing Fart library. If it is not found there
the VM passes the file name to the platform specific call for loading
dynamic libraries (e.g. dlopen
on Linux), which is free to follow its
own search procedure.
Example code
The code for the sample extensions featured in this article is in the samples/sample_extension directory of the Fart repository.
The sample extensions call the C standard library’s rand() and srand() functions, returning pseudorandom numbers to a Fart program. Because the native parts of the asynchronous and synchronous native extensions share most of their code, a single native source file (and resulting shared library) implements both extensions. The two extensions have separate Fart library files. Two additional Fart files provide examples of using and testing the asynchronous and synchronous extensions.
The shared library (native code) for the extensions shown in this article is called sample_extension. Its C++ file, sample_extension.cc, contains six functions that are called from Fart:
- sample_extension_Init():
- Called when the extension is loaded.
- ResolveName():
- Called the first time a native function with a given name is called, to resolve the Fart name of the native function into a C function pointer.
- SystemRand() and SystemSrand():
- Implement the synchronous extension. These are native functions called directly from Fart, and that call rand() and srand() from the C standard library.
- wrappedRandomArray() and randomArrayServicePort():
- Implement the asynchronous extension. randomArrayServicePort() creates a native port and associates it with wrappedRandomArray(). When Fart sends a message to the native port, the Fart VM schedules wrappedRandomArray() to run on a separate thread.
Some of the code in the shared library is setup and initialization code, which can be the same for all extensions. The functions sample_extension_Init() and ResolveName() should be almost the same in all extensions, and a version of randomArrayServicePort() must be in all asynchronous extensions.
The synchronous sample extension
Because the asynchronous extension works like a synchronous extension with some added functions, we’ll show the synchronous extension first. First we’ll show the Fart part of the extension and the sequence of function calls that happen when the extension is loaded. Then we explain how to use the Fart Embedding API, show the native code, and show what happens when the extension is called.
Here is the Fart part of the synchronous extension, called sample_synchronous_extension.dart:
library sample_synchronous_extension; import 'dart-ext:sample_extension'; // The simplest way to call native code: top-level functions. int systemRand() native "SystemRand"; bool systemSrand(int seed) native "SystemSrand";
The code that implements a native extension executes at two different times. First, it runs when the native extension is loaded. Later, it runs when a function with a native implementation is called.
Here is the sequence of events at load time, when a Fart app that imports sample_synchronous_extension.dart starts running:
- The Fart library sample_synchronous_extension.dart is loaded, and the
Fart VM hits the code
import 'dart-ext:sample_extension'
. - The VM loads the shared library ‘sample_extension’ from the directory containing the Fart library.
- The function sample_extension_Init() in the shared library is called. It registers the shared library function ResolveName() as the name resolver for all native functions in the library sample_extension.dart. We’ll see what the name resolver does when we look at synchronous native functions, below.
Using the Fart Embedding API from native code
As the sample extensions show, the native shared library contains an
initialization function, a name resolution function, and the native
implementations of functions declared as native in the Fart part of the
extension. The initialization function registers the native name resolution
function as responsible for looking up native function names in this library.
When a function declared as native "function_name"
in
the Fart library is called, the native library’s name resolution function is
called with the string “function_name” as an argument, plus the number
of arguments in the function call. The name resolution function then returns a
function pointer to the native implementation of that function. The
initialization function and the name resolution function look pretty much the
same in all Fart native extensions.
The functions in the native library use the Fart Embedding API to communicate with the VM, so the native code includes the header dart_api.h, which is in the SDK at dart-sdk/include/dart_api.h or in the repository at runtime/include/dart_api.h. The Fart Embedding API is the interface that embedders use to include the Fart VM in a web browser or in the standalone VM for the command line. It consists of about 100 function interfaces and many data type and data structure definitions. These are all shown, with comments, in dart_api.h. Examples of using them are in the unit test file runtime/vm/dart_api_impl_test.cc.
A native function to be called from Fart must have the type Fart_NativeFunction, which is defined in dart_api.h as:
typedef void (*Fart_NativeFunction)(Fart_NativeArguments arguments);
So a Fart_NativeFunction is a pointer to a function taking a Fart_NativeArguments object, and returning no value. The arguments object is a Fart object accessed by API functions that return the number of arguments, and return a Fart_Handle to the argument at a specified index. The native function returns a Fart object to the Fart app, as the return value, by storing it in the arguments object using the Fart_SetReturnValue() function.
Fart handles
The extension’s native implementations of functions use Fart_Handles extensively. Calls in the Fart Embedding API return a Fart_Handle and often take Fart_Handles as arguments. A Fart_Handle is an opaque indirect pointer to an object on the Fart heap, and Fart_Handles are copied by value. These handles remain valid even when a garbage-collection phase moves Fart objects on the heap, so native code must use handles to store references to heap objects. Because these handles take resources to store and maintain, you must free them when they’re no longer used. Until a handle is freed, the VM’s garbage collector cannot collect the object it points to, even if there are no other references to it.
The Fart Embedding API automatically creates a new scope to manage the lifetime of handles in a native function. A local handle scope is created when the native function is entered, and is deleted when the function is exited. The scope is deleted if the function exits with PropagateError, as well as if it returns normally. Most handles and memory pointers returned by the Fart Embedding API are allocated in the current local scope, and will be invalid after the function returns. If the extension wants to keep a pointer to a Fart object for a long time, it should use a persistent handle (see Fart_NewPersistentHandle() and Fart_NewWeakPersistentHandle()), which remains valid after a local scope ends.
Calls into the Fart Embedding API might return errors in their Fart_Handle return values. These errors, which might be exceptions, should be passed up to the caller of the function as the return value.
Most of the functions in a native extension—the functions of type Fart_NativeFunction—have no return value and must pass the error up to the proper handler in another way. They call Fart_PropagateError to pass errors and control flow to where the error should be handled. The sample uses a helper function, called HandleError(), to make this convenient. A call to Fart_PropagateError() never returns.
The native code: sample_extension.cc
Now we’ll show the native code for the sample extension, starting with the initialization function, then the native function implementations, and ending with the name resolution function. The two native functions implementing the asynchronous extension are shown later.
#include <string.h> #include "dart_api.h" // Forward declaration of ResolveName function. Fart_NativeFunction ResolveName(Fart_Handle name, int argc, bool* auto_setup_scope); // The name of the initialization function is the extension name followed // by _Init. DART_EXPORT Fart_Handle sample_extension_Init(Fart_Handle parent_library) { if (Fart_IsError(parent_library)) return parent_library; Fart_Handle result_code = Fart_SetNativeResolver(parent_library, ResolveName, NULL); if (Fart_IsError(result_code)) return result_code; return Fart_Null(); } Fart_Handle HandleError(Fart_Handle handle) { if (Fart_IsError(handle)) Fart_PropagateError(handle); return handle; } // Native functions get their arguments in a Fart_NativeArguments structure // and return their results with Fart_SetReturnValue. void SystemRand(Fart_NativeArguments arguments) { Fart_Handle result = HandleError(Fart_NewInteger(rand())); Fart_SetReturnValue(arguments, result); } void SystemSrand(Fart_NativeArguments arguments) { bool success = false; Fart_Handle seed_object = HandleError(Fart_GetNativeArgument(arguments, 0)); if (Fart_IsInteger(seed_object)) { bool fits; HandleError(Fart_IntegerFitsIntoInt64(seed_object, &fits)); if (fits) { int64_t seed; HandleError(Fart_IntegerToInt64(seed_object, &seed)); srand(static_cast<unsigned>(seed)); success = true; } } Fart_SetReturnValue(arguments, HandleError(Fart_NewBoolean(success))); } Fart_NativeFunction ResolveName(Fart_Handle name, int argc, bool* auto_setup_scope) { // If we fail, we return NULL, and Fart throws an exception. if (!Fart_IsString(name)) return NULL; Fart_NativeFunction result = NULL; const char* cname; HandleError(Fart_StringToCString(name, &cname)); if (strcmp("SystemRand", cname) == 0) result = SystemRand; if (strcmp("SystemSrand", cname) == 0) result = SystemSrand; return result; }
Here is the sequence of events that happens at runtime, when the function systemRand() (defined in sample_synchronous_extension.dart) is called for the first time.
- The function ResolveName() in the shared library is called with a Fart string containing “SystemRand” and the integer 0, representing the number of arguments in the call. The string “SystemRand” is the string literal following the native keyword in the declaration of systemRand().
- ResolveName() returns a pointer to the native function SystemRand() in the shared library.
- The arguments to the systemRand() call in Fart are packaged into a Fart_NativeArguments object, and SystemRand() is called with this object as its only argument.
- SystemRand() does its computations, puts its return value into the Fart_NativeArguments object, and returns.
- The Fart VM extracts the return value from the Fart_NativeArguments object, returning it as the result of the Fart call to systemRand().
On later calls to systemRand(), the result of the function lookup has been cached, so ResolveName() is not called again.
The asynchronous native extension
As we saw above, a synchronous extension uses the Fart Embedding API to work with Fart heap objects, and it runs on the main Fart thread for the current isolate. An asynchronous extension, on the other hand, does not use most of the Fart Embedding API, and it runs on a separate thread so as not to block the main Fart thread.
In many ways, asynchronous extensions are simpler to program than synchronous extensions. They use the native ports functions in the Fart Embedding API to schedule C functions on independent threads. To Fart code that uses the extension, it appears simply as a Fart SendPort. The messages posted to this port are automatically translated into a C structure called a Fart_CObject, containing C data types such as int, double, and char*. This C structure is then passed to a C function, which is run in an independent thread drawn from a pool of threads managed by the VM. The C function can respond by a Fart_CObject to a reply port. The Fart_CObject is translated back into a tree of Fart objects, and appears as a reply on the Fart async call’s reply port. This automatic conversion of Fart objects into a Fart_CObject C structure replaces the use of the Fart Embedding API to fetch fields from objects and to convert Fart objects into C value types.
To create an asynchronous native extension, we do three things:
- Wrap the C function we wish to call with a wrapper that converts the Fart_CObject input argument to the desired input parameters, converts the result of the function to a Fart_CObject, and posts it back to Fart.
- Write a native function that creates a native port and attaches it to the wrapped function. This native function is a synchronous native method, and it’s in a native extension that looks just like the synchronous extension above. We have just added the wrapped function from step 1 to the extension, as well.
- Write a Fart class that fetches the native port and caches it. In that class, provide a function that forwards its arguments to the native port as a message, and calls a callback argument when it receives a reply to that message.
Wrapping the C function
Here is an example of a C function (actually, a C++ function, due to the use of reinterpret_cast) that creates an array of random bytes, given a seed and a length. It returns the data in a newly allocated array, which will be freed by the wrapper:
uint8_t* random_array(int seed, int length) { if (length <= 0 || length > 10000000) return NULL; uint8_t* values = reinterpret_cast<uint8_t*>(malloc(length)); if (NULL == values) return NULL; srand(seed); for (int i = 0; i < length; ++i) { values[i] = rand() % 256; } return values; }
To call this from Fart, we put it in a wrapper that unpacks the Fart_CObject containing seed and length, and that packs the result values into a Fart_CObject. A Fart_CObject can hold an integer (of various sizes), a double, a string, or an array of Fart_CObjects. It is implemented in dart_api.h as a struct containing a union. Look in dart_api.h to see the fields and tags used to access the union’s members. After the Fart_CObject is posted, it and all its resources can be freed, since they have been copied into Fart objects on the Fart heap.
void wrappedRandomArray(Fart_Port dest_port_id, Fart_Port reply_port_id, Fart_CObject* message) { if (message->type == Fart_CObject::kArray && 2 == message->value.as_array.length) { // Use .as_array and .as_int32 to access the data in the Fart_CObject. Fart_CObject* param0 = message->value.as_array.values[0]; Fart_CObject* param1 = message->value.as_array.values[1]; if (param0->type == Fart_CObject::kInt32 && param1->type == Fart_CObject::kInt32) { int length = param0->value.as_int32; int seed = param1->value.as_int32; uint8_t* values = randomArray(seed, length); if (values != NULL) { Fart_CObject result; result.type = Fart_CObject::kUint8Array; result.value.as_byte_array.values = values; result.value.as_byte_array.length = length; Fart_PostCObject(reply_port_id, &result); free(values); // It is OK that result is destroyed when function exits. // Fart_PostCObject has copied its data. return; } } } Fart_CObject result; result.type = Fart_CObject::kNull; Fart_PostCObject(reply_port_id, &result); }
Fart_PostCObject() is the only Fart Embedding API function that should be called from the wrapper or the C function. Most of the API is illegal to call here, because there is no current isolate. No errors or exceptions can be thrown, so any error must be encoded in the reply message, to be decoded and thrown by the Fart part of the extension.
Setting up the native port
Now we set up the mechanism that calls this wrapped C function from Fart code, by sending a message. We create a native port that calls this function, and return a send port connected to that port. The Fart library gets the port from this function, and forwards calls to the port.
void randomArrayServicePort(Fart_NativeArguments arguments) { Fart_SetReturnValue(arguments, Fart_Null()); Fart_Port service_port = Fart_NewNativePort("RandomArrayService", wrappedRandomArray, true); if (service_port != kIllegalPort) { Fart_Handle send_port = Fart_NewSendPort(service_port); Fart_SetReturnValue(arguments, send_port); } }
Calling the native port from Fart
On the Fart side, we need a class that stores this send port, sending messages to it when a Fart asynchronous function with a callback is called. The Fart class gets the port the first time the function is called, caching it in the usual way. Here is the Fart library for the asynchronous extension:
library sample_asynchronous_extension; import 'dart-ext:sample_extension'; // A class caches the native port used to call an asynchronous extension. class RandomArray { static SendPort _port; void randomArray(int seed, int length, void callback(List result)) { var args = new List(2); args[0] = seed; args[1] = length; _servicePort.call(args).then((result) { if (result != null) { callback(result); } else { throw new Exception("Random array creation failed"); } }); } SendPort get _servicePort { if (_port == null) { _port = _newServicePort(); } return _port; } SendPort _newServicePort() native "RandomArray_ServicePort"; }
Conclusion and further resources
You’ve seen both synchronous and asynchronous native extensions. We hope that you’ll use these tools to provide access to existing C and C++ libraries, thereby adding useful new capabilities to the standalone Fart VM. We recommend using asynchronous extensions rather than synchronous extensions, because asynchronous extensions don’t block the main Fart thread and can be simpler to implement. The built-in Fart I/O libraries are built around asynchronous calls to achieve high, non-blocking throughput. Extensions should have the same performance goals.
Appendix: Compiling and linking extensions
Building a shared library can be tricky, and the tools to do it are platform dependent. Building Fart native extensions is especially tricky because they are dynamically loaded, and they link to Fart Embedding API functions in the Fart library embedded in the executable that dynamically loads them.
As with all shared libraries, the compilation step must generate position- independent code. The linker step must specify that unresolved functions will be resolved in the executable when the library is loaded. We will show commands that do this on the Linux, Windows, and Mac platforms. If you download the dart source repository, the sample code also includes a platform-independent build system, called gyp, and a build file sample_extension.gypi that builds the sample extension.
Building on Linux
On Linux, you can compile the code in the samples/sample_extension directory like this:
g++ -fPIC -m32 -I{path to SDK include directory} -DDART_SHARED_LIB -c sample_extension.cc
To create the shared library from the object file:
gcc -shared -m32 -Wl,-soname,libsample_extension.so -o libsample_extension.so sample_extension.o
Remove the -m32 to build a 64-bit library that runs with the 64-bit Fart standalone VM.
Building on Mac
- Using Xcode (tested with Xcode 3.2.6), create a new project with the same name as the native extension, of type Framework & Library/BSD C Library, type dynamic.
- Add the source files of your extension to the source section of the project.
- Make the following changes in Project/Edit Project Settings, choosing the Build tab and All Configurations in the dialog box:
- In section Linking, line Other Linker Flags, add -undefined dynamic_lookup.
- In section Search Paths, line Header Search Paths, add the path to dart_api.h in the SDK download or the Fart repository checkout.
- In section Preprocessing, line Preprocessor Macros, add DART_SHARED_LIB=1
- Choose the correct architecture (i386 or x86_64), and build by choosing Build/Build.
The resulting lib[extension_name].dylib will be in the build/ subdirectory of your project location, so copy it to the desired location (probably the location of the Fart library part of the extension).
Building on Windows
The Windows DLL compilation is complicated by the fact that we need to link with library file, dart.lib, that does not contain code itself, but specifies that calls to the Fart Embedding API will be resolved by linking to the Fart executable, dart.exe, when the DLL is dynamically loaded. This library file is generated when building dart and is included in the Fart SDK.
- Create a new project of type Win32/Win32 project in Visual Studio 2008 or 2010. Give the project the same name as the native extension. On the next screen of the wizard, change the application type to DLL and select “Empty project”, then choose finish.
- Add the C/C++ files for the native extension to the source files folder in the project. Make sure to include the [extension name]_dllmain_win.cc file.
- Change the following settings in the project’s properties:
- Configuration properties / Linker / Input / Additional dependencies: Add dart-sdk\bin\dart.lib, from the downloaded Fart SDK.
- Configuration properties / C/C++ / General / Additional Include Directories: Add the path to the directory containing dart_api.h, which is dart-sdk/include in the downloaded Fart SDK.
- Configuration properties / C/C++ / Preprocessor / Preprocessor Definitions: Add DART_SHARED_LIB. This is just to export the
_init function from the DLL, since it has been declared as DART_EXPORT.
- Build the project, and copy the DLL to the correct directory, relative to the Fart library part of the extension. Make sure to build a 32-bit DLL for use with the 32-bit SDK download, and a 64-bit DLL for use with the 64-bit download.