# JNR-FFI calls native libraries from a Java interface instead of JNI glue

> JNR-FFI is a Java library for loading native libraries without writing JNI code by hand or using tools such as SWIG, where a Java interface describes the C function signatures and LibraryLoader binds them at runtime. The library is dual licensed under Apache-2.0 and LGPL-3.0, and it is published to Maven Central as com.github.jnr:jnr-ffi.

**jnr/jnr-ffi** — Java Abstracted Foreign Function Layer

- Repository: https://github.com/jnr/jnr-ffi
- Stars: 1,327 · Forks: 160
- Language: Java
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/jnr-jnr-ffi

## A Java interface is the binding

JNR-FFI is described as a Java library for loading native libraries without writing JNI code by hand, or using tools such as SWIG. The README's example makes the shape of it obvious, because the binding is an ordinary Java interface with method signatures that mirror the C ones.

```java
import jnr.ffi.LibraryLoader;

public class HelloWorld {
    public interface LibC { // A representation of libC in Java
        int puts(String s); // mapping of the puts function, in C `int puts(const char *s);`
    }

    public static void main(String[] args) {
        LibC libc = LibraryLoader.create(LibC.class).load("c");

        libc.puts("Hello World!");
    }
}
```

The interface LibC is a representation of libC in Java, and the comment on the puts method records the C declaration it maps to. The loader is created from the interface class and then asked to load the library, in this case the C library itself, and the result is used as if it were an ordinary object.

What that eliminates is the boilerplate of the JNI route. A JNI binding means a set of native methods declared with the native keyword, a matching C file with the exported symbols, a header to keep in sync, and a build step to produce the shared library. The interface approach writes the signature once, in a form a Java developer already reads, and the machinery resolves it at runtime.

The other thing it eliminates is the tool chain. The README names SWIG specifically, and the point is not only that SWIG is extra machinery but that SWIG needs to know about the header, and headers describe an interface, whereas a Java interface describes a binding. When the library is a dynamic library discovered at runtime rather than something with an installed development package, the header approach is awkward in a way the interface approach is not.

## The load happens at runtime, which is the trade

The one-line example hides the main design decision, which is that the binding is resolved at runtime rather than at compile time.

LibraryLoader.create(LibC.class) takes the interface, produces a loader for it, and load("c") finds and opens the native library and returns a proxy implementing that interface. Nothing about the C library was known when the Java code was compiled.

The consequences run in both directions. On the positive side, the same Java code can bind to a library whose path differs between machines, and the interface can be written without a header being present at build time, which is what makes it usable for a library that is installed as a binary only. It also means you can bind several libraries with the same interface shape without recompiling.

On the negative side, the resolution of each call goes through machinery rather than through a direct native method invocation. The cost of that is real, and it is the reason a project with a JNI binding already written and already tuned would not switch to this for performance reasons.

The negative side also includes a class of error that a compiled JNI binding cannot have. If the interface says a function has a different signature from the one in the library, the mismatch is found at binding time or at call time, and the failure is a linkage error rather than a compile error. A native method with the wrong signature is a different kind of problem, because the JVM resolves native methods by name and signature and reports a UnsatisfiedLinkError at the point of use. Either way the mistake is caught at runtime, and the interface approach at least fails close to where the mistake is written.

The repository layout reflects a library with a real native test surface. There is a libtest/ directory at the top level, which is the native side of the test fixtures, plus src/ for the Java code, docs/ for the user documentation, a pom.xml, and a Maven wrapper, mvnw, at the root. The .mvn/ directory holds the wrapper configuration.

## One dependency coordinate for Maven and for Gradle

The installation section gives the coordinates and the three build tool variants, which is the practical first thing any user needs.

For Apache Maven the dependency is com.github.jnr, jnr-ffi, with a version, and the README links the Maven Central listing through the badge. For Gradle in Kotlin the syntax is implementation("com.github.jnr:jnr-ffi:x.y.z"), and for Gradle in Groovy it is implementation 'com.github.jnr:jnr-ffi:x.y.z'.

The group id is the interesting part of the coordinate. com.github.jnr is the group used by the jnr projects, and it is not the same as the GitHub organisation, which is jnr. That is a naming decision from an era when the project was distributed through a non standard route, and it means the artifact you get from Maven Central is the one the jnr project publishes, under a group id that reads like a personal namespace.

The README points to the user documentation at docs/README.md for more details, and the repository has a docs/ directory at the top level, so the documentation lives with the code rather than on a separate site.

The release process is documented too, which is a small sign of a project with a stable release convention. To deploy artifacts to Sonatype Central you run mvn clean deploy, and to deploy release artifacts to Maven Central you update the release version to remove the -SNAPSHOT suffix and run mvn clean deploy -Prelease. After the release artifacts have been published, you update the release version to the next snapshot.

The two most recent releases are 2.3.1 on 2026-08-20 and 2.3.2 on 2026-09-15, and the last push to master was on 2026-09-15, so the cadence is regular.

## Dual licensed, which is unusual and useful

The LICENSE section in the README is a two-way licence, and that is worth reading carefully because it is not a common arrangement for a library of this kind.

The first option is the Apache License, Version 2.0, with the standard permission notice. The alternative is that you can redistribute it and modify it under the terms of the GNU Lesser General Public License, version 3 of the License, or any later version, with the distribution warranty disclaimer that the code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY.

So you take your pick between a permissive Apache-2.0 and a copyleft LGPL-3.0. That is unusual, and it is unusual in a useful way. The Apache option is the one most commercial users will take, and the LGPL option exists so that the library can be incorporated into a project that has to stay LGPL compatible, which matters for a library whose entire purpose is to be embedded in other software.

The repository listing marks the licence as a custom one that GitHub cannot classify, which is consistent with a dual licence, since GitHub's classifier has a single value for this field and cannot represent an either-or.

There is a distinction to be careful about. The dual licence applies to this library, jnr-ffi, the Java code that binds to native libraries. It is a separate question whether the native libraries you bind to permit that, and that is a question about those libraries' licences rather than about this one.

The project name itself is worth decoding, because it explains the design. JNR stands for Java Native Runtime, and the FFI stands for Foreign Function Interface, which is the term for calling functions in another language from your own.

## Where JNR-FFI is the wrong choice

The first wrong case is a library that already has a good JNI binding. The Java ecosystem has maintained JNI bindings for a large number of native libraries, and if one exists for what you need, it is faster, it is typed at the compiler level, and it is already tested against the library's edge cases. Writing your own interface to a library that already has one is work without a payoff.

The second wrong case is a hot path. The overhead of the resolution machinery is real and it applies to every call. A library where you call one function a few thousand times is unaffected. A library where you call it a few million times per second is a different situation, and there a compiled JNI layer or a hand written binding is the right tool.

The third wrong case is a native library with a complex interface. A C API with hundreds of functions, opaque handles, callbacks, struct by value arguments, varargs, and platform specific variants, is a lot to express as Java interfaces, and the mapping from C types to Java types has edge cases. The interface approach works best for a bounded, well behaved API surface.

The fourth wrong case is a requirement for compile time verification of the binding. The point of JNI's verbosity is that the native side is written out explicitly, so it can be reviewed before release. An interface based binding is checked at runtime, and a project whose risk model treats a mismatched native signature as a release blocker rather than a runtime error will want the JNI route.

## How it differs from JNI and from SWIG

The comparison worth making is with the two things the README names as what you would otherwise use, because they fail in different ways.

JNI is the low level interface. You declare a native method in a Java class, implement it in a C file that you compile and package, and the JVM links the two by name and signature. The advantage is that the call overhead is close to a direct native call. The cost is that every function needs three things written for it, a Java declaration, a C implementation and a header, and the three have to be kept in agreement by hand.

SWIG is the generator. You write an interface file describing the C API, and SWIG parses it and emits the Java class and the C glue. The advantage is that the description is written once, in something close to C. The cost is that the tool is in the build, the interface file has its own syntax, and the generated code is a layer between you and the library.

JNR-FFI takes the interface file idea and makes the interface a Java interface. So the description is written once, in a language the Java developer already uses, and there is no generator step and no generated code to read through. The trade is that the binding is resolved at runtime and the dispatch goes through the library's own machinery.

There is a fourth option worth naming, which is the process-based one. A native library can be driven by a separate process over a pipe, which is slower but removes the linking problem entirely and keeps memory and crashes isolated. For a library that is occasionally called and where robustness matters more than latency, that is a legitimate answer, and it is the one that requires no FFI work at all.

## Conclusion

JNR-FFI fits a Java project that needs to bind to a C library the JVM has no JNI binding for, and that wants the binding expressed as a readable Java interface instead of a generated stub plus a header parser, and it fits a project that must support several native libraries with different signatures. It does not fit a project where a maintained JNI binding already exists for the library you need, and it does not fit a team that needs the very highest performance, since reflection based dispatch carries a cost that a hand written JNI layer avoids. Before adopting it, check the user documentation in docs/README.md for the calling conventions your native library needs, and note that the release cadence is steady with 2.3.2 on 2026-09-15 and the last push on the same day, and that the dual Apache and LGPL licensing means you can use it under either.

## FAQ

### What is JNR-FFI and what does it replace?

JNR-FFI is a Java library for loading native libraries without writing JNI code by hand, or using tools such as SWIG. A Java interface describes the C function signatures and LibraryLoader binds them at runtime, so the description is written once in a language the Java developer already uses.

### How do I add JNR-FFI to my project?

For Maven use the com.github.jnr groupId with the jnr-ffi artifactId and a version. For Gradle in Kotlin use implementation("com.github.jnr:jnr-ffi:x.y.z"), and for Gradle in Groovy use implementation 'com.github.jnr:jnr-ffi:x.y.z'. The package is published on Maven Central.

### What license is JNR-FFI released under?

It is dual licensed. You may use it under the Apache License, Version 2.0, or alternatively under the terms of the GNU Lesser General Public License version 3 or any later version. The README carries both notices in the LICENSE section.

### How is a native library loaded with JNR-FFI?

You declare a Java interface mirroring the C signatures, then call LibraryLoader.create(LibC.class).load("c") to open the native library and get a proxy implementing that interface. The binding is resolved at runtime rather than at compile time, which is what removes the need for JNI glue or a SWIG step.

### How do I publish a JNR-FFI release?

Run mvn clean deploy to deploy artifacts to Sonatype Central, and to deploy release artifacts to Maven Central, update the release version to remove the -SNAPSHOT suffix and run mvn clean deploy -Prelease, then update the version to the next snapshot. The recent releases are 2.3.1 on 2026-08-20 and 2.3.2 on 2026-09-15.

## Sources

- [Issues](https://github.com/jnr/jnr-ffi/issues)
- [jnr/jnr-ffi on GitHub](https://github.com/jnr/jnr-ffi)
- [README](https://github.com/jnr/jnr-ffi/blob/master/README.md)
- [Releases](https://github.com/jnr/jnr-ffi/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jnr-jnr-ffi
