node-ffi calls C libraries from JavaScript without a C++ addon
GitHub describes it as Node.js Foreign Function Interface. The repository metadata lists JavaScript as its primary language. The metadata lists the MIT license. This article stays within the project description and details documented in the GitHub repository README.
At a glance
- What is it?
- node-ffi loads a shared library at runtime and marshals arguments through libffi, so you can call native code from plain JavaScript. The payoff is zero C++ to write; the cost is a segfault class of failure the README refuses to soften.
- Who is it for?
- Adopt node-ffi when you need one narrow call into a library that ships no Node module, on Linux, OS X, Windows or Solaris, and you can live with the marshalling rules. Do not adopt it for a hot loop, for a 64-bit integer you care about, or for a team that has never debugged native code, since the last push was on 2023-07-15 and the newest release, 2.3.0, dates from 2018-12-15.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Probably not. The repository last received commits 39 months ago, on July 15, 2023.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.
Star history
- node-ffi/node-ffi4,320 ★ · -2
| Repository | 2026-08-13 | 2026-10-04 |
|---|---|---|
| node-ffi/node-ffi | 4.3k | 4.3k |
-2 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
ffi.Library resolves a .so or .dll at runtime, not at build time
The interesting decision in node-ffi happens in one line of configuration: you name a library and you describe each function's signature, and from that point the call goes straight from JavaScript into native code. There is no generated header, no compile step in your project, and no C++ file to maintain. That is the whole pitch, and for the narrow case of binding a library that has no Node package it is a good one.
The mechanism underneath is libffi, the portable foreign function interface library. It is bundled with the package, so you do not install it yourself, which removes the most common reason a native binding fails to build on a fresh machine. What remains is the signature table, where you state the return type first and the argument types in a nested array. libffi uses that table to build the native call at runtime, which is why an incorrect entry produces a crash rather than an exception.
The signature syntax comes from ref, the companion project, and the README defers to that project's type documentation rather than restating it. In practice you will meet the plain C names: int, double, string, void, and pointers you build with the ref helpers. A second project, ref-struct, is what handles structures, and the test script in package.json pulls in ref-array as well, which tells you where the layout support lives: in siblings, not in node-ffi itself.
The README opens with a segfault warning, and that warning is the design
Most READMEs for native bindings talk about type safety. This one leads with a warning: node-ffi assumes you know what you are doing, and you can pretty easily create situations where you will segfault the interpreter, and and without C debugger skills you will not know what is going on. That is unusually candid, and it is also accurate about what the library is. A JavaScript type error gives you a stack trace. A wrong pointer width or a signature that claims one argument type and receives another gives you a dead process.
So the failure mode is not an error message, it is the absence of one. If you declare a function as taking a string and hand it a buffer, or you pass a pointer where libffi expects a struct by value, there is no layer between your JavaScript and the C ABI to catch the mismatch. The interpreter dies, and the stack trace you get points at the call site rather than the cause.
The second use case the README describes is the opposite one: augmenting Node.js itself with C code. It points at example/factorial as the demonstration. The claim is that ffi takes care of translating types across the JavaScript and C boundary, which otherwise adds reams of boilerplate to otherwise simple C. The trade is that the safety a compiled addon would give you at compile time is replaced by nothing at all at runtime.
Two ways in: npm install ffi, or node-gyp rebuild in a clone
The short path is a single npm install. Before you run it, the README asks that the platform's build tools are in place, pointing at the node-gyp installation notes, which is the one prerequisite that trips people up: ffi is a native addon, so a compiler toolchain has to exist on the machine even though you wrote no C yourself.
$ npm install ffiThe second path compiles from source, which is what you do when you need a change or when you want to see what lands in the build directory. Install node-gyp first, then clone and rebuild:
$ npm install -g node-gyp$ git clone git://github.com/node-ffi/node-ffi.git
$ cd node-ffi
$ node-gyp rebuildA third command exists for contributors rather than users. package.json defines the test script as a rebuild inside the test directory followed by mocha, so running the suite means compiling test fixtures too, not just requiring the module:
node-gyp rebuild --directory test && mocha -gc --reporter specThe repository layout explains what that rebuild touches. src and binding.gyp hold the native side, lib holds the JavaScript wrapper that package.json points at through its main field, deps carries bundled third-party code, and example holds the runnable cases: abs.js, sqlite.js and the factorial directory.
Passing null to ffi.Library reaches functions in the current process
The example in the README does two things in eight lines, and the second one gets skipped by most people. The first declares libm and binds ceil, then calls it on 1.5 and gets 2 back. That is the ordinary case.
var ffi = require('ffi');
var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2The second passes null as the library name, and the README says this lets you access just functions in the current process, using atoi as the example. That is more than a curiosity. It means you do not need a shared object on disk at all to reach a C function that your own native addon already linked in, which turns node-ffi into a bridge between two native pieces rather than only a bridge to a system library.
It also changes the failure profile. With a named library, a missing file is at least a clean load-time failure. With null, the symbol lookup depends entirely on what is already in the process, so a name that resolves on your machine can be absent on a machine built with different flags. Nothing in the README checks for a null result at bind time.
64-bit integers come back as strings because V8 cannot hold them
This is the detail a skim misses and it shapes how you write the calling code. V8 stores integers that fit in 32 bits as 32-bit integers, and anything larger goes into a double-precision floating point number. Floating point is imprecise, so a 64-bit identifier silently loses precision. node-ffi's answer is that methods dealing with 64-bit integers return strings and accept strings as parameters.
The consequence is that every call touching a large integer carries a parse and a stringify. For a file offset or a pointer-sized handle returned from C, that cost is a design choice you accept. In a loop over millions of rows it is not, and you should be looking at a compiled binding instead.
The repository confirms the shape of the API surface. package.json declares version 2.3.0, main as ./lib/ffi, and 5 runtime dependencies: bindings pinned at ~1.2.0 for the binary lookup, debug at 2, nan at ^2.12.0 for the native addon layer, ref at 1 for the type system, and ref-struct at 1 for structures. The engines field pins node at 0.10.0 or newer, which is the honest summary of the project's vintage: a floor far behind any current Node release, and one more reason to read the lockfile before you depend on it.
Call overhead is orders of magnitude above a compiled binding
The README compares a hard-coded binding of strtoul to an FFI version of the same function and concludes the native hard-coded binding is orders of magnitude faster. Then it draws the rule directly: do not just use the C version of a function just because it is faster, and make FFI calls worth it.
That is unusually good advice to find in a project README, because it argues against the library's own scope. The overhead comes from the table lookup, the type marshalling and the libffi call preparation that happen on every single invocation. A function called once at startup is free. A function called once per array element, or inside a render loop, will dominate your profile.
The same section sets the boundary for the segfault warning. Both caveats point the same direction: node-ffi is a tool for reaching a specific function, not a way to port a library. If you find yourself binding twenty functions because you want the whole API surface in JavaScript, the compiled route costs more up front and less on every call after that. The hard-coded comparison the README describes is measured against exactly that kind of binding.
The alternative is a N-API addon you write once and maintain forever
The difference in approach is not a matter of speed alone. node-ffi resolves the library and its signatures at runtime, from JavaScript, so the configuration is data and the failure is a dead process. A N-API addon inverts that: you write C or C++, declare the functions you want, and the Node build compiles them into a binary at install time. The signature error becomes a compiler error, the library version becomes a build dependency you pin, and the 64-bit integer stays an integer because you control the marshalling.
What you trade away is exactly what node-ffi sells. Every call site needs a header, a build entry and a maintained wrapper function, and shipping that to several platforms means several toolchains. People searching for an alternative to node-ffi usually want that guarantee back, accepting the code.
The middle position is a prebuilt addon for the specific library, if one exists on npm. That gives a Node-only API without either the runtime crash risk or a local compile. The choice is about who absorbs the risk: node-ffi puts it in every call, an N-API addon puts it in your build step once.
Editorial conclusion
Adopt node-ffi when you need one narrow call into a library that ships no Node module, on Linux, OS X, Windows or Solaris, and you can live with the marshalling rules. Do not adopt it for a hot loop, for a 64-bit integer you care about, or for a team that has never debugged native code, since the last push was on 2023-07-15 and the newest release, 2.3.0, dates from 2018-12-15. Verify first that the shared library is present on every machine you plan to ship to, because ffi.Library resolves by name at load time and a missing file or an unresolved symbol takes down the whole process rather than raising an exception you can catch. If that check fails, write the N-API addon instead.
Frequently asked questions
What is node ffi?
node-ffi is a Node.js addon for loading and calling dynamic libraries using pure JavaScript, so you can create bindings to native libraries without writing any C++ code. It handles the translation of types across JavaScript and C.
Do I need to install libffi separately for node-ffi?
No. The README states that libffi comes bundled with node-ffi and does not need to be installed on your system, though you do need the necessary build tools for your platform.
Which platforms and Node versions does node-ffi support?
Linux, OS X, Windows and Solaris are listed as supported platforms, and package.json declares an engines floor of node 0.10.0 or newer. The README describes testing on node v0.6, v0.8, v0.9 and v0.10.
How does node-ffi handle 64-bit integers?
Because V8 stores large integers as imprecise floating point numbers, the node-ffi methods that deal with 64-bit integers return strings and accept strings as parameters. You pay a parse and stringify on every such call.
Is node-ffi faster than a compiled native binding?
No. The README compares a hard-coded binding of strtoul against an FFI version and reports the hard-coded version as orders of magnitude faster, advising that FFI calls be limited to those worth the overhead.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/node-ffi-node-ffi)Star history chart for your README
[](https://hysenlabs.com/projects/node-ffi-node-ffi)