7

FFI with no glue code!

 1 year ago
source link: https://coder-mike.com/blog/2022/10/16/ffi-generator-library/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

FFI with no glue code!

FFI with no glue code!

TL;DR: Microvium’s snapshotting paradigm allows a library to generate the FFI glue code, so you don’t have to.


How bad can it be?

Foreign function interfaces (FFIs) are notoriously difficult in JavaScript. If you take a look at the Node-API documentation for Node.js, you’ll see how confusing it can be. Take a look at this “simple” example of a C function that adds 2 JavaScript numbers together:

// addon.cc
#include <node.h>
namespace demo {
using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
// This is the implementation of the "add" method
// Input arguments are passed using the
// const FunctionCallbackInfo<Value>& args struct
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// Check the number of arguments passed.
if (args.Length() < 2) {
// Throw an Error that is passed back to JavaScript
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"Wrong number of arguments").ToLocalChecked()));
return;
// Check the argument types
if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"Wrong arguments").ToLocalChecked()));
return;
// Perform the operation
double value =
args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
Local<Number> num = Number::New(isolate, value);
// Set the return value (using the passed in
// FunctionCallbackInfo<Value>&)
args.GetReturnValue().Set(num);
void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "add", Add);
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
// addon.cc
#include <node.h>

namespace demo {

using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// This is the implementation of the "add" method
// Input arguments are passed using the
// const FunctionCallbackInfo<Value>& args struct
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Check the number of arguments passed.
  if (args.Length() < 2) {
    // Throw an Error that is passed back to JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong number of arguments").ToLocalChecked()));
    return;
  }

  // Check the argument types
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong arguments").ToLocalChecked()));
    return;
  }

  // Perform the operation
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // Set the return value (using the passed in
  // FunctionCallbackInfo<Value>&)
  args.GetReturnValue().Set(num);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

Yikes! And that’s even before we start talking about garbage collection, handles, and scopes.

The above code is mostly glue code because most of it exists solely to interface between JavaScript and C++, rather than adding any functionality of its own.

Microvium’s approach is fundamentally different

I’ll explain the Microvium approach by going through an example.

In Microvium, the first thing you probably want to do is have your C++ host firmware call a JavaScript function, so I’ll cover that example first:

// main.js
import { generate, exportToC } from './lib/ffi.js'
exportToC('void', 'myFunctionToCallFromC', [], () => {
// ... function code here ...
generate();
// main.js

import { generate, exportToC } from './lib/ffi.js'

exportToC('void', 'myFunctionToCallFromC', [], () => {
  // ... function code here ...
});

generate();

Here I’m using a library called ffi.js, which I’ll explain later. It exposes an exportToC function which has the following signature:

function exportToC(returnType: Typename, funcName: string, params: Array<[paramType: Typename, paramName: string]>);
function exportToC(returnType: Typename, funcName: string, params: Array<[paramType: Typename, paramName: string]>);

Combined with the function generate, the function exportToC automatically generates the required glue code for the exported function.

How is this possible?

Well, remember that in Microvium, the top-level module code runs at compile time, not runtime. And by default, it also has access to Node.js modules1 such as fs, so it can access the file system.

So, let’s compile this in a terminal. I’m using --output-bytes here so I can get the literal snapshot bytes to paste into the C++ in a moment.

$ microvium main.js --output-bytes
Output generated: main.mvm-bc
154 bytes
{0x06,0x1c,0x06,0x00,0x9a,0x00,0xf2,0x75,0x03,0x00,0x00,0x00,0x1c,0x00,0x1c,0x00,0x24,0x00,0x24,0x00,0x2a,0x00,0x2c,0x00,0x80,0x00,0x8a,0x00,0xff,0xff,0x49,0x00,0xfe,0xff,0x7d,0x00,0x89,0x00,0x85,0x00,0x01,0x00,0x31,0x00,0x00,0x00,0x05,0x40,0x70,0x75,0x73,0x68,0x00,0x00,0x0d,0x50,0x04,0x31,0x30,0x30,0x88,0x1d,0x00,0x6b,0x12,0x6f,0x67,0x01,0x60,0x00,0x2f,0x50,0x05,0x88,0x19,0x00,0x89,0x00,0x00,0x88,0x1d,0x00,0x6b,0xa0,0x88,0x19,0x00,0x06,0xa0,0x10,0x12,0xe0,0x70,0x04,0x67,0x67,0x01,0x60,0x89,0x00,0x00,0x10,0x12,0x6b,0x11,0x78,0x01,0xa0,0x67,0x10,0x10,0x07,0x6c,0x10,0xa2,0x67,0x67,0x76,0xe2,0x00,0x00,0x00,0x03,0x50,0x01,0x01,0x60,0x00,0x0c,0x00,0x19,0x00,0x02,0x00,0x19,0x00,0x01,0x00,0x08,0xc0,0x05,0x00,0x05,0x00,0x31,0x00,0x39,0x00,0x04,0xd0,0x05,0x00,0x03,0x00}
$ microvium main.js --output-bytes
Output generated: main.mvm-bc
154 bytes
{0x06,0x1c,0x06,0x00,0x9a,0x00,0xf2,0x75,0x03,0x00,0x00,0x00,0x1c,0x00,0x1c,0x00,0x24,0x00,0x24,0x00,0x2a,0x00,0x2c,0x00,0x80,0x00,0x8a,0x00,0xff,0xff,0x49,0x00,0xfe,0xff,0x7d,0x00,0x89,0x00,0x85,0x00,0x01,0x00,0x31,0x00,0x00,0x00,0x05,0x40,0x70,0x75,0x73,0x68,0x00,0x00,0x0d,0x50,0x04,0x31,0x30,0x30,0x88,0x1d,0x00,0x6b,0x12,0x6f,0x67,0x01,0x60,0x00,0x2f,0x50,0x05,0x88,0x19,0x00,0x89,0x00,0x00,0x88,0x1d,0x00,0x6b,0xa0,0x88,0x19,0x00,0x06,0xa0,0x10,0x12,0xe0,0x70,0x04,0x67,0x67,0x01,0x60,0x89,0x00,0x00,0x10,0x12,0x6b,0x11,0x78,0x01,0xa0,0x67,0x10,0x10,0x07,0x6c,0x10,0xa2,0x67,0x67,0x76,0xe2,0x00,0x00,0x00,0x03,0x50,0x01,0x01,0x60,0x00,0x0c,0x00,0x19,0x00,0x02,0x00,0x19,0x00,0x01,0x00,0x08,0xc0,0x05,0x00,0x05,0x00,0x31,0x00,0x39,0x00,0x04,0xd0,0x05,0x00,0x03,0x00}

A side effect of running this command is that it generated the files App_ffi.hpp and App_ffi.cpp for us, which contains the generated glue code for this example.

So now that we have the generated glue code and the snapshot bytes, we can use this in a minimal C++ project2:

#include "App_ffi.hpp"
const uint8_t snapshot[] = {0x06,0x1c,0x06,0x00,0x9a,0x00,0xf2,0x75,0x03,0x00,0x00,0x00,0x1c,0x00,0x1c,0x00,0x24,0x00,0x24,0x00,0x2a,0x00,0x2c,0x00,0x80,0x00,0x8a,0x00,0xff,0xff,0x49,0x00,0xfe,0xff,0x7d,0x00,0x89,0x00,0x85,0x00,0x01,0x00,0x31,0x00,0x00,0x00,0x05,0x40,0x70,0x75,0x73,0x68,0x00,0x00,0x0d,0x50,0x04,0x31,0x30,0x30,0x88,0x1d,0x00,0x6b,0x12,0x6f,0x67,0x01,0x60,0x00,0x2f,0x50,0x05,0x88,0x19,0x00,0x89,0x00,0x00,0x88,0x1d,0x00,0x6b,0xa0,0x88,0x19,0x00,0x06,0xa0,0x10,0x12,0xe0,0x70,0x04,0x67,0x67,0x01,0x60,0x89,0x00,0x00,0x10,0x12,0x6b,0x11,0x78,0x01,0xa0,0x67,0x10,0x10,0x07,0x6c,0x10,0xa2,0x67,0x67,0x76,0xe2,0x00,0x00,0x00,0x03,0x50,0x01,0x01,0x60,0x00,0x0c,0x00,0x19,0x00,0x02,0x00,0x19,0x00,0x01,0x00,0x08,0xc0,0x05,0x00,0x05,0x00,0x31,0x00,0x39,0x00,0x04,0xd0,0x05,0x00,0x03,0x00};
void main() {
// Load the JavaScript app from the snapshot
App* app = new App(snapshot, sizeof snapshot);
// Run the myFunctionToCallFromC function
app->myFunctionToCallFromC();
#include "App_ffi.hpp"

const uint8_t snapshot[] = {0x06,0x1c,0x06,0x00,0x9a,0x00,0xf2,0x75,0x03,0x00,0x00,0x00,0x1c,0x00,0x1c,0x00,0x24,0x00,0x24,0x00,0x2a,0x00,0x2c,0x00,0x80,0x00,0x8a,0x00,0xff,0xff,0x49,0x00,0xfe,0xff,0x7d,0x00,0x89,0x00,0x85,0x00,0x01,0x00,0x31,0x00,0x00,0x00,0x05,0x40,0x70,0x75,0x73,0x68,0x00,0x00,0x0d,0x50,0x04,0x31,0x30,0x30,0x88,0x1d,0x00,0x6b,0x12,0x6f,0x67,0x01,0x60,0x00,0x2f,0x50,0x05,0x88,0x19,0x00,0x89,0x00,0x00,0x88,0x1d,0x00,0x6b,0xa0,0x88,0x19,0x00,0x06,0xa0,0x10,0x12,0xe0,0x70,0x04,0x67,0x67,0x01,0x60,0x89,0x00,0x00,0x10,0x12,0x6b,0x11,0x78,0x01,0xa0,0x67,0x10,0x10,0x07,0x6c,0x10,0xa2,0x67,0x67,0x76,0xe2,0x00,0x00,0x00,0x03,0x50,0x01,0x01,0x60,0x00,0x0c,0x00,0x19,0x00,0x02,0x00,0x19,0x00,0x01,0x00,0x08,0xc0,0x05,0x00,0x05,0x00,0x31,0x00,0x39,0x00,0x04,0xd0,0x05,0x00,0x03,0x00};

void main() {
  // Load the JavaScript app from the snapshot
  App* app = new App(snapshot, sizeof snapshot);
  
  // Run the myFunctionToCallFromC function
  app->myFunctionToCallFromC();
}

How easy is that! We spun up the runtime engine and called a JavaScript function in 2 lines of code!

Let’s extend this example to make it call from JavaScript back to C++. Let’s say that we want to add two numbers together, and print the result:

import { generate, exportToC, importFromC } from './lib/ffi.js'
const add = importFromC('int', 'add', [['int', 'x'], ['int', 'y']]);
const print = importFromC('void', 'print', [['string', 'msg']]);
exportToC('void', 'myFunctionToCallFromC', [], () => {
const x = add(1, 2);
print(`The sum is ${x}`);
generate();
import { generate, exportToC, importFromC } from './lib/ffi.js'

const add = importFromC('int', 'add', [['int', 'x'], ['int', 'y']]);
const print = importFromC('void', 'print', [['string', 'msg']]);

exportToC('void', 'myFunctionToCallFromC', [], () => {
  const x = add(1, 2);
  print(`The sum is ${x}`);
});

generate();

If we peak inside the generated “App_ffi.hpp” for this one, we’ll see it now has these lines as well:

// ...
extern int32_t add(App* app, int32_t x, int32_t y); // Must be implemented elsewhere
extern void print(App* app, std::string msg); // Must be implemented elsewhere
// ...
// ...
extern int32_t add(App* app, int32_t x, int32_t y); // Must be implemented elsewhere
extern void print(App* app, std::string msg); // Must be implemented elsewhere
// ...

It’s automatically generated the function signatures of the imported functions, and all the glue code required to give the JavaScript code the ability to call these functions.

So, what’s left is for us to implement add and print3:

#include <iostream>
#include "App_ffi.hpp"
using namespace std;
using namespace mvm;
const uint8_t snapshot[] = {0x06,0x1c,0x06,0x00,0xd2,0x00,0xb7,0x71,0x03,0x00,0x00,0x00,0x1c,0x00,0x20,0x00,0x28,0x00,0x28,0x00,0x2e,0x00,0x32,0x00,0xb4,0x00,0xc2,0x00,0xff,0xff,0xfe,0xff,0xff,0xff,0x65,0x00,0xfe,0xff,0x99,0x00,0xc1,0x00,0xbd,0x00,0x01,0x00,0x3d,0x00,0x35,0x00,0x05,0x40,0x70,0x75,0x73,0x68,0x00,0x00,0x0c,0x40,0x54,0x68,0x65,0x20,0x73,0x75,0x6d,0x20,0x69,0x73,0x20,0x00,0x00,0x00,0x02,0x60,0x00,0x00,0x02,0x60,0x01,0x00,0x0d,0x50,0x04,0x31,0x30,0x30,0x88,0x1d,0x00,0x6b,0x12,0x6f,0x67,0x01,0x60,0x00,0x2f,0x50,0x05,0x88,0x19,0x00,0x89,0x00,0x00,0x88,0x1d,0x00,0x6b,0xa0,0x88,0x19,0x00,0x06,0xa0,0x10,0x12,0xe0,0x70,0x04,0x67,0x67,0x01,0x60,0x89,0x00,0x00,0x10,0x12,0x6b,0x11,0x78,0x01,0xa0,0x67,0x10,0x10,0x07,0x6c,0x10,0xa2,0x67,0x67,0x76,0xe2,0x00,0x00,0x00,0x1c,0x50,0x05,0x88,0x19,0x00,0x89,0x01,0x00,0x01,0x07,0x08,0x78,0x03,0xa0,0x89,0x02,0x00,0x01,0x88,0x3d,0x00,0x13,0x6c,0x78,0x02,0x67,0x67,0x01,0x60,0x0c,0x00,0x4d,0x00,0x51,0x00,0x19,0x00,0x02,0x00,0x19,0x00,0x01,0x00,0x08,0xc0,0x05,0x00,0x05,0x00,0x35,0x00,0x55,0x00,0x04,0xd0,0x05,0x00,0x03,0x00};
void main() {
App* app = new App(snapshot, sizeof snapshot);
app->myFunctionToCallFromC();
int32_t add(App* app, int32_t x, int32_t y) {
return x + y;
void print(App* app, string msg) {
cout << msg << endl;
#include <iostream>
#include "App_ffi.hpp"

using namespace std;
using namespace mvm;

const uint8_t snapshot[] = {0x06,0x1c,0x06,0x00,0xd2,0x00,0xb7,0x71,0x03,0x00,0x00,0x00,0x1c,0x00,0x20,0x00,0x28,0x00,0x28,0x00,0x2e,0x00,0x32,0x00,0xb4,0x00,0xc2,0x00,0xff,0xff,0xfe,0xff,0xff,0xff,0x65,0x00,0xfe,0xff,0x99,0x00,0xc1,0x00,0xbd,0x00,0x01,0x00,0x3d,0x00,0x35,0x00,0x05,0x40,0x70,0x75,0x73,0x68,0x00,0x00,0x0c,0x40,0x54,0x68,0x65,0x20,0x73,0x75,0x6d,0x20,0x69,0x73,0x20,0x00,0x00,0x00,0x02,0x60,0x00,0x00,0x02,0x60,0x01,0x00,0x0d,0x50,0x04,0x31,0x30,0x30,0x88,0x1d,0x00,0x6b,0x12,0x6f,0x67,0x01,0x60,0x00,0x2f,0x50,0x05,0x88,0x19,0x00,0x89,0x00,0x00,0x88,0x1d,0x00,0x6b,0xa0,0x88,0x19,0x00,0x06,0xa0,0x10,0x12,0xe0,0x70,0x04,0x67,0x67,0x01,0x60,0x89,0x00,0x00,0x10,0x12,0x6b,0x11,0x78,0x01,0xa0,0x67,0x10,0x10,0x07,0x6c,0x10,0xa2,0x67,0x67,0x76,0xe2,0x00,0x00,0x00,0x1c,0x50,0x05,0x88,0x19,0x00,0x89,0x01,0x00,0x01,0x07,0x08,0x78,0x03,0xa0,0x89,0x02,0x00,0x01,0x88,0x3d,0x00,0x13,0x6c,0x78,0x02,0x67,0x67,0x01,0x60,0x0c,0x00,0x4d,0x00,0x51,0x00,0x19,0x00,0x02,0x00,0x19,0x00,0x01,0x00,0x08,0xc0,0x05,0x00,0x05,0x00,0x35,0x00,0x55,0x00,0x04,0xd0,0x05,0x00,0x03,0x00};

void main() {
  App* app = new App(snapshot, sizeof snapshot);
  app->myFunctionToCallFromC();
}

int32_t add(App* app, int32_t x, int32_t y) {
  return x + y;
}

void print(App* app, string msg) {
  cout << msg << endl;
}

That’s all! The glue code generated in App_ffi handles the conversions between JavaScript values and C++ values, such as converting the JavaScript string to an std string for the print.

What about dynamic types?

What if we don’t have a specific type we want to pass between JavaScript and C++? The FFI library provides a solution for this as well: Any.

Let’s say we want to make add polymorphic, so it can add either strings or integers. Instead of importing it with the type int, we can use the type any:

const add = importFromC('any', 'add', [['any', 'x'], ['any', 'y']]);
const add = importFromC('any', 'add', [['any', 'x'], ['any', 'y']]);

Then on the C++ side, it might look like this (either adding integers or concatenating strings):

Any add(App* app, Any x, Any y) {
if (x.type() == VM_T_NUMBER) {
return app.newInt32(x.toInt32() + x.toInt32());
} else {
return app.newString(x.toString() + x.toString());
Any add(App* app, Any x, Any y) {
  if (x.type() == VM_T_NUMBER) {
    return app.newInt32(x.toInt32() + x.toInt32());
  } else {
    return app.newString(x.toString() + x.toString());
  }
}

The Any type is actually a reference type: it’s a garbage-collection-safe reference to a value in the JavaScript VM. So it can also be used to safely interact with objects and arrays in JavaScript.

It’s about the concept, not the library

As of this writing, the FFI library used here (ffi.js) is not included with Microvium. It’s an early-stage concept library, which you can find here. There’s still more thought and functionality that needs to go into it before I’m ready to call it the “standard way” of interacting with Microvium and release it alongside the Microvium engine.

But I think the cool part here is not the FFI library itself, but the fact that the snapshotting paradigm facilitates libraries like this. It doesn’t need to be baked-in behavior of the engine — if you don’t like the way my FFI library does things, you can write your own4 ! The possibilities are endless. Do you want your library to also generate the makefile? You can! Do you want it to generate main.cpp? You can! Do you want it to work with C instead of C++? You can! Or rather… given a large enough community of users, you hope that someone else has done it already and shared their solution on npm or somewhere.

The concept runs deeper than just a typical code generator. Of course, anyone can write a code generator for node.js that generates the glue code for you, but it’s not possible in node.js to create a library that allows you to write code like this:

const add = importFromC('int', 'add', [['int', 'x'], ['int', 'y']]);
const print = importFromC('void', 'print', [['string', 'msg']]);
exportToC('void', 'myFunctionToCallFromC', [], () => {
const x = add(1, 2);
print(`The sum is ${x}`);
const add = importFromC('int', 'add', [['int', 'x'], ['int', 'y']]);
const print = importFromC('void', 'print', [['string', 'msg']]);

exportToC('void', 'myFunctionToCallFromC', [], () => {
  const x = add(1, 2);
  print(`The sum is ${x}`);
});

Why? Because this example combines runtime and compile-time code in the same place. The function exportToC creates a bridge between C++ and JavaScript, and encapsulates the details of that bridge. We don’t care how the library works, as long as it adheres to the interface contract — the contract on both sides of the bridge — the contract in both JavaScript and C++.

It is the snapshotting paradigm of Microvium that enables a library that performs this kind of encapsulation and abstraction of a communication link. And interfacing between JavaScript and C++ is only the beginning of what you can do with this! There are some other things on the horizon that take this to the next level.


  1. The Microvium compiler runs on Node.js, and exposes the Node.js API via a proxy layer 

  2. The FFI library I’ve made here uses C++ rather than C so that it can use RAII for automatically dealing with garbage collector handles. 

  3. Remember that Microvium doesn’t support any runtime I/O out of the box. 

  4. Or rather, raise a ticket on GitHub so we can improve the same library. 


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK