1 of 15

Fast FFI for Node.js

ofrobots@google.com, mattloring@google.com

03/03/17

2 of 15

Objective

Provide a simple, fast, productive, pure JavaScript mechanism for a large subset of native module use cases

3 of 15

Motivation: Native Use Cases

  1. Reusing an existing C/C++ library
  2. Building tools that inspect VM state (profilers, debuggers, etc)
  3. Building high performance library requiring native speed
  4. Avoid locking down the event loop by running on a thread pool (libuv, pthreads, etc) and calling back into JS when operation completes

4 of 15

Current Solutions: Direct use of V8 API

Local<Object> TranslateString(Isolate* isolate, char* str) {� EscapableHandleScope handle_scope(isolate);� Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();� return handle_scope.Escape(result);�}

  • Requires understanding of VM internals
  • Easily Broken by V8 api changes
    1. Release every 6 weeks
    2. Api change -> rewrite code, abi change -> recompile
  • Requires JS developers to write native code

5 of 15

Current Solutions: Native Abstractions for Node (NAN)

Local<Object> TranslateString(char* str) {� Nan::EscapableHandleScope handle_scope;� Local<String> result = Nan::New(str).ToLocalChecked();� return handle_scope.Escape(result);�}

  • Some convenience methods for abstracting VM internals
  • Some shielding from V8 API changes
    • Some API breaking changes are too great for NAN to cover up

6 of 15

Current Solutions: node-ffi

var ffi = require('ffi');�var libm = ffi.Library('libm', { 'ceil': [ 'double', [ 'double' ] ] });�libm.ceil(1.5);

  • Insulates users from V8 internals
  • Does not require module author to write native code
  • Prohibitively slow

7 of 15

Requirements

  1. Provide efficient type translation across the JS/native boundary
  2. Shield developers from VM internal concepts and representations
  3. Require little or no native code to wrap an existing native library
  4. Provide a good story for common async workflows
  5. API should be familiar to users of node-ffi

8 of 15

Synchronous Translation

FFI Compiler

Function Ptr

DLL Name

JSFunction

Call Function Ptr

FromJS

FromJS

FromJS

ToJS

JS Return Value

JS Arg1

JS Arg2

JS Arg3

Fast FFI Module

dlsym

dlopen

Function Signature

Function Name

sig translation

libfoo.so

void foo();

9 of 15

Synchronous Example

var libadd = ffi.Library('libadd.so');

var plusTwoJS = libadd.getFunction('plusTwo', [ ffi.Int32Type, [ ffi.Int32Type ] ]);

var four = plusTwoJS(2);

int plusTwo(int n);

libadd.so

JSFunction (plusTwoJS)

Call plusTwo

JSNumberToInt32

Int32ToJSNumber

JSNumber

JSNumber

10 of 15

Asynchronous Translation

FFI Compiler

Function Ptr

DLL Name

JSFunction

Function Ptr

FromJS

FromJS

FromJS

JS Callback

JS Arg1

JS Arg2

Fast FFI Module

dlsym

dlopen

Function Signature

Function Name

sig translation

libuv thread

bind

CallNativeFunction

11 of 15

Asynchronous Example

var libadd = ffi.Library('libadd.so');

var plusTwoAsync = libadd.getFunctionAsync('plusTwo', [ ffi.Int32Type, [ ffi.Int32Type ] ]);

plusTwoAsync(2, function(four) { ... });

int plusTwo(int n);

libadd.so

JSFunction (plusTwoJSAsync)

plusTwo

JSNumberToInt32

JSFunctionToC

JSNumber

JSFunction

libuv thread

bind

CallNativeFunction

12 of 15

Design: Function Translation

FromJS

JSFunction

C Function

JSFunction

ToJS

ToJS

ToJS

FromJS

Return Value

Arg1

Arg2

Arg3

JSExitFrame

JSEntryFrame

13 of 15

Status

  1. Implemented prototype using turbofan IR
  2. Type support for int32, float32, float64, string, buffer, structs, and functions
  3. Implemented nanomsg on top of prototype
  4. Reimplementing full FFI pipeline on top of Code Stub Assembler IR to better align with other efforts on wasm/dom use cases

14 of 15

Design: Type Translations from JS

Input

Conversion

Output

JSNumber

-> double -> cast

int32, int64, float32, float64

JSObject

-> translate fields by value

Struct Pointer

JSFunction

-> wrap in JS entry logic + type conversion

Function Pointer

Buffer

-> unbox + copy

TypedArray

JSForeign

-> unbox

Void Pointer

15 of 15

Design: Type Translations to JS

Input

Conversion

Output

int32, int64, float32, float64

-> to double

JSNumber

Struct Pointer

-> translate fields by value

JSObject

Function Pointer

-> ffi

JSFunction

TypedArray

-> box in buffer

Buffer

Void Pointer

-> box in foreign

JSForiegn