-
Notifications
You must be signed in to change notification settings - Fork 0
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.
Check the Android native toolchain first:
supernote-module doctor --type jniJNI 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-moduleOr generate one directly:
supernote-module add native-math-jni --type jni --yesThe examples on this page assume:
- Package name:
native-math-jni - JavaScript name:
NativeMath
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.
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.
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++.
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++.
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.
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
#endifThen 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++.
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, orextern "C"exported functions - Export markers in
.cfiles
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.
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.
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.
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.
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.
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.
Run a build whenever you add, remove, rename, or change the signature of an exported function:
supernote-module validate native-math-jni --buildFor the full compiler and Gradle output:
supernote-module validate native-math-jni --build --verboseThe build scans the C++ sources, regenerates bindings, and then compiles the native library.
Commit your plugin before either command.
supernote-module update native-math-jni
supernote-module remove native-math-jniUpdate 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.
Run:
supernote-module doctor --type jniRead 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.
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 --verboseDo 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.
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.
Those files are generated and replaceable. Restore the change from version control, document why it is required, and reapply it after later Updates.