Skip to content

Native JNI Modules

s edited this page Aug 3, 2026 · 1 revision

Native JNI Modules

The CLI calls this type Native JNI Module.

Use it when you need the performance, control, or library support of C or C++, but still want a normal Android module that can work with Kotlin, Java, and Android APIs and expose a Promise-based interface to JavaScript.

This is the middle ground between a Native Module and a JSI Module. JavaScript does not call your C or C++ function directly. The generated Kotlin and JNI layers handle the connection, while you write the C or C++ implementation.

Generate the module

Check the Android native toolchain first:

supernote-module doctor --type jni

JNI generation requires CMake 3.22.1 or newer and an Android NDK whose Clang compiler supports the generated C23 and C++23 code for arm64-v8a.

Then run the guided interface and choose Native JNI Module:

supernote-module

Or generate one directly:

supernote-module add native-math-jni --type jni --yes

The examples on this page assume:

  • Package name: native-math-jni
  • JavaScript name: NativeMath

What the generator handles

The generator creates the repetitive parts needed to call C or C++ through a React Native Native Module, including:

  • The Kotlin or Java-facing Native Module
  • JNI bindings and native registration
  • Native library loading
  • CMake configuration
  • React Native package registration
  • The JavaScript wrapper and TypeScript declarations

You do not need to write JNI function names, JNI_OnLoad, registration tables, or the generated bridge yourself.

Where to write your code

Write C and C++ files below:

local_modules/native-math-jni/android/src/main/cpp/

Update preserves this complete directory, including any files you add and starter files you intentionally delete.

The other generated Android and package files are owned by the generator and may be replaced by Update.

Export a C++ function

Place // @SupernoteExport immediately before an ordinary top-level C++ function definition:

// @SupernoteExport
double add(double left, double right) {
    return left + right;
}

Call it from JavaScript or TypeScript through a Promise:

import NativeMath from 'native-math-jni';

const total = await NativeMath.add(20, 22);

The generator finds the marked definition, creates the JNI binding, and adds the TypeScript declaration.

Do not write your own JNI wrapper for an exported function. The whole point of the generator is that the marked C++ function remains normal C++.

Supported values

Exported C++ functions currently support these values by value:

C++ JavaScript or TypeScript JNI call behavior
bool boolean Promise when returned
double number Promise when returned
std::string string Promise when returned
void return void Called without awaiting a result

Strings are treated as UTF-8.

A void function has no Promise to resolve or reject. JavaScript calls it without await, and a native exception is logged by the generated module rather than returned to JavaScript. Use a value-returning function when JavaScript needs confirmation that the operation succeeded.

Pointers, references, structs, arrays, arbitrary objects, and other C++ types cannot cross the generated boundary directly. Keep the exported function small and convert to your internal types after the call enters C++.

Rename the JavaScript function

By default, the JavaScript name matches the C++ function name.

Use an explicit name when needed:

// @SupernoteExport(name = "greet")
std::string make_greeting(std::string name) {
    return "Hello, " + name;
}
const message = await NativeMath.greet('Ziv');

Export names must be unique and valid JavaScript property names.

C and C++ files

Files ending in .cc, .cpp, or .cxx may contain exported functions.

Files ending in .c compile as C23 and can be used for helper code, but they cannot contain @SupernoteExport functions. Exported bindings are generated in C++.

When C++ calls a C helper, use a normal header with extern "C" guards:

#ifndef FAST_MATH_H
#define FAST_MATH_H

#ifdef __cplusplus
extern "C" {
#endif

double fast_add(double left, double right);

#ifdef __cplusplus
}
#endif

#endif

Then call it from an exported C++ function:

#include "fast_math.h"

// @SupernoteExport
double add(double left, double right) {
    return fast_add(left, right);
}

This lets most of an implementation remain C while keeping the generated export boundary in C++.

Export rules

An exported function must be a normal top-level definition with explicitly named parameters.

The generator does not currently support:

  • Overloaded functions
  • Functions inside namespaces
  • Function templates
  • Pointer or reference parameters and returns
  • Variadic or default arguments
  • static, inline, constexpr, or extern "C" exported functions
  • Export markers in .c files

noexcept is supported.

These restrictions apply only to the exported boundary. Your internal C and C++ implementation can use other types, classes, templates, namespaces, pointers, and libraries as normal.

Work with Kotlin, Java, and Android

A Native JNI Module is packaged as a normal Android Native Module. This makes it the appropriate choice when C or C++ performs the low-level or expensive work, while the Android side still needs to deal with permissions, services, lifecycle, files supplied by Android APIs, or existing Kotlin and Java code.

The Kotlin, JNI, loading, and registration files created by the generator are managed files. Custom changes to those generated files may be overwritten by Update.

When a feature needs extra custom communication between your C++ implementation and Kotlin or Java beyond the generated exported functions, treat that as manual Android/JNI integration: commit first, keep the changes clearly documented, and expect to review or reapply them after an Update.

Performance and batching

JNI does not remove the React Native bridge. JavaScript still communicates with the generated Native Module, and the Native Module then enters C or C++ through JNI.

This makes JNI a good fit when each call asks C or C++ to perform a meaningful amount of work. It is a poor fit for an API that sends one point, byte, or tiny update at a time.

Prefer:

JavaScript sends one complete operation
→ C/C++ performs the work
→ JavaScript receives one result

Avoid:

JavaScript repeatedly calls C/C++ for every tiny step

C or C++ can make the implementation itself faster or give you lower-level control, but crossing both boundaries still has a cost. Batch calls and measure the complete operation rather than benchmarking only the inner C++ function.

Long-running work

Value-returning JNI exports appear as Promises in JavaScript, which keeps the JavaScript API asynchronous. That does not automatically make every native implementation safe or parallel.

You are still responsible for deciding where expensive work runs, avoiding inappropriate Android-thread blocking, and making shared native state thread-safe. A Promise describes how JavaScript receives the result; it is not a replacement for correct native threading.

Add existing C or C++ code

Source files added below android/src/main/cpp/ are part of the preserved implementation tree. The generated build inventories supported C and C++ source files when it regenerates the bindings.

External dependencies, prebuilt libraries, and custom linker settings may require manual CMake or Gradle changes. Those generated build files are owned by the generator and may be replaced by Update. Commit before editing them and be prepared to reapply the changes.

The generator does not guarantee that a third-party native library is compatible with Android API 27, arm64-v8a, the target Supernote firmware, or PluginHost's linker environment.

Other native languages

Rust, Zig, Go, and other languages that can expose a C-compatible ABI can potentially be used behind a small C or C++ wrapper.

The generator does not install those toolchains, create their Android builds, package their runtimes, or write the wrapper for you. From the generator's point of view, the exported function is still C++ and any other language is an implementation detail behind it.

For most of these integrations, JNI remains the safer choice when the feature also needs Android APIs or asynchronous calls.

Validate after changing exports

Run a build whenever you add, remove, rename, or change the signature of an exported function:

supernote-module validate native-math-jni --build

For the full compiler and Gradle output:

supernote-module validate native-math-jni --build --verbose

The build scans the C++ sources, regenerates bindings, and then compiles the native library.

Update and remove

Commit your plugin before either command.

supernote-module update native-math-jni
supernote-module remove native-math-jni

Update preserves the complete android/src/main/cpp/ tree. It may replace the package files, JavaScript wrapper, TypeScript declarations, generated Kotlin/JNI code, CMake and Gradle files, registration, loader, metadata, and generated README.

Update cannot rename the module type, package name, JavaScript name, Android namespace, or package version.

Remove deletes the complete generated package, including the preserved C and C++ source tree.

Common problems

Doctor reports a CMake or NDK failure

Run:

supernote-module doctor --type jni

Read the first failed requirement. The generated module expects CMake 3.22.1 or newer and NDK Clang support for C23 and C++23 targeting aarch64-linux-android27.

An exported function does not appear

Check that:

  • The marker is immediately before the definition.
  • The file ends in .cc, .cpp, or .cxx.
  • The function is top-level and not overloaded.
  • Every parameter is named.
  • All parameters and the return value use supported by-value types.
  • The export name is unique.

Then run:

supernote-module validate native-math-jni --build --verbose

The generated call cannot find its native method

Do not add handwritten JNI symbols. This normally means the generated binding is stale, the function signature is unsupported, registration failed, or the native build did not complete.

Run a verbose build and inspect the first binding, compiler, linker, or loading error rather than adding a second manual bridge.

The native library fails to load

Confirm that it was built for arm64-v8a, all of its shared-library dependencies are available, and it does not require a newer Android API than the target device provides.

A successful C++ compilation does not guarantee that every dependent library can be loaded by PluginHost.

Update removed a custom CMake or Gradle change

Those files are generated and replaceable. Restore the change from version control, document why it is required, and reapply it after later Updates.