The Foreign Function Interface (FFI) lets your Alloy app call native C functions directly from JavaScript. This is useful for performance-critical code, for reusing existing C libraries, or for reaching low-level functionality that isn't exposed through a JavaScript API.
Platform Support: Like the rest of Alloy, FFI is available on Emery (Pebble Time 2) and Gabbro (Pebble Time 2 round).
An FFI binding has three parts:
ffi block in manifest.json that lists the C sources and declares
each function's signature.Natives global in your JavaScript, through which the functions are
called.Add one or more C files alongside your main.js (for example
src/embeddedjs/add.c). The C function names must match the names you declare
in the manifest:
// add.c
#include <stdint.h>
int32_t add(int32_t a, int32_t b) {
return a + b;
}
int32_t addSquares(int32_t a, int32_t b) {
return (a * a) + (b * b);
}
Functions can take and return buffers and strings as well as numbers:
// hello.c
#include <stdint.h>
#include <string.h>
// Fill the caller-provided buffer with bytes derived from `greeting`.
void hello(char *greeting, uint8_t *bytes, uint32_t length) {
size_t len = strlen(greeting);
for (uint32_t i = 0; i < length; i++) {
bytes[i] = (uint8_t)greeting[i % len];
}
}
In src/embeddedjs/manifest.json, add an ffi block listing your C sources
and the functions with their argument and return types:
{
"include": [
"$(MODDABLE)/examples/manifest_mod.json",
"$(MODDABLE)/examples/manifest_typings.json"
],
"modules": {
"*": "./main"
},
"ffi": {
"sources": [
"./add.c",
"./hello.c"
],
"functions": {
"add": {
"arguments": [ "int32_t", "int32_t" ],
"returns": "int32_t"
},
"addSquares": {
"arguments": [ "int32_t", "int32_t" ],
"returns": "int32_t"
},
"hello": {
"arguments": [ "char *", "uint8_t*", "uint32_t" ],
"returns": "void"
}
}
}
}
| C type | JavaScript value |
|---|---|
int32_t, uint8_t, uint32_t |
Number |
char* (argument) |
String |
char*, const char* (return) |
String |
uint8_t*, void* |
ArrayBuffer (pass .buffer from a typed array or DataView) |
void |
undefined |
Numeric arguments are passed by value. Buffers are passed by reference, so a C
function can read from and write into an ArrayBuffer you pass in.
The C entry point (src/c/mdbl.c) must pass the generated fxBuildFFI symbol
when it creates the Moddable machine:
#include <pebble.h>
int main(void) {
Window *w = window_create();
window_stack_push(w, true);
moddable_createMachine(&(ModdableCreationRecord){
.recordSize = sizeof(ModdableCreationRecord),
#ifdef PBL_DEBUG
.flags = kModdableCreationFlagDebug,
#endif
.fxBuildFFI = fxBuildFFI,
});
window_destroy(w);
return 0;
}
All declared functions are available on the global Natives object:
console.log("Hello, FFI.");
trace(`Natives.add(2, 3) = ${ Natives.add(2, 3) }\n`);
trace(`Natives.addSquares(3, 4) = ${ Natives.addSquares(3, 4) }\n`);
const bytes = new Uint8Array(5);
Natives.hello("hello", bytes.buffer, bytes.length);
trace(`bytes = ${ bytes }\n`);
Wrap calls in a try/catch to handle errors thrown across the boundary:
try {
const result = Natives.add(2, 3);
} catch (error) {
console.log("FFI Error: " + error);
}
The FFI mechanism comes from the Moddable SDK. See the
XS FFI documentation
for the full set of supported types and behaviors, and the
helloffi example
for a complete project.