-
Notifications
You must be signed in to change notification settings - Fork 0
JSI Modules
Use the native environment when the module needs an existing C or C++ library, low-level file or memory work, or performance-sensitive code that belongs naturally in C++.
This environment can provide the module's complete implementation. It does not need Kotlin or Java.
You are also not creating a special “JSI module.” V2 always exposes JavaScript through generated JSI. C/C++ is the implementation environment you write; JSI is the connection the generator builds around it.
supernote-module doctor
supernote-module add local-math --starter cpp --yesThe starter creates a C++ example under:
local_modules/local-math/android/src/main/cpp/
You can add .c, .cc, .cpp, .cxx, .h, and .hpp files below that
native root. Update preserves the complete directory.
The generated CMake build uses C23 for C files and C++23 for C++ files.
For marked C++ declarations, the generator creates:
- JSI functions and native-object wrappers;
- JavaScript argument checking and value conversion;
- TypeScript declarations;
- Promise and worker plumbing for explicit async calls;
- JNI routes when native code calls marked Kotlin or Java declarations;
- native build and React Native registration; and
- safe runtime, feature, and object lifetime handling.
You write ordinary C and C++. Your functions do not take a jsi::Runtime, and
your classes do not inherit from a generated base class.
Place the exact marker comment on its own line immediately before a top-level C++ function:
#include <cstdint>
// @SupernotePluginExport
std::int32_t add(std::int32_t left, std::int32_t right) {
return left + right;
}After generation, import the package and call it from JavaScript or TypeScript:
import localMath from 'local-math';
function onCalculate() {
const total: number = localMath.add(20, 22);
// Use total here.
}The declaration name is the JavaScript name. There is no rename argument, so
name the C++ function the way you want it to appear in index.d.ts.
C23 is fully supported as ordinary implementation code. A C library can expose its normal C ABI to a small marked C++ wrapper:
// checksum.c
#include <stdint.h>
int32_t checksum_adjust(int32_t value) {
return value + 7;
}// feature.cpp
#include <cstdint>
extern "C" std::int32_t checksum_adjust(std::int32_t value);
// @SupernotePluginExport
std::int32_t adjustedChecksum(std::int32_t value) {
return checksum_adjust(value);
}The extern "C" declaration is ordinary C/C++ interop. It is not a Supernote
marker. The marked JavaScript boundary currently belongs in C++23; putting a
Supernote marker directly on a .c declaration is rejected.
Most implementation code should not have a Supernote marker:
double clampRatio(double value) {
return std::max(0.0, std::min(value, 1.0));
}Public C++ visibility does not expose anything to JavaScript. Call ordinary helpers through normal C++ just as you would in any other project.
Use SupernotePluginInternal only when the generator needs to create a typed,
feature-aware route—for example, when generated C++ needs to call a marked
Kotlin function. It is not a replacement for normal C++ function calls.
// @SupernotePluginInternal
std::int32_t rebuildIndex(std::int32_t page) {
return page + 1;
}An internal declaration is deliberately absent from JavaScript and
index.d.ts.
A normal export returns before JavaScript continues:
JavaScript
-> argument conversion
-> your C++ function
-> result conversion
-> JavaScript continues
This is useful for short calculations and quick native-state queries.
It is also allowed to read a file, wait, or perform a large computation synchronously. The important consequence is that JavaScript cannot do anything else until the function returns. If the call takes two seconds, the plugin can appear frozen for two seconds.
Choose based on the behavior you want, and measure it on the target device.
Add explicit async intent when JavaScript should receive a Promise and the implementation should run away from the JS thread:
#include <cstddef>
#include <cstdint>
#include <vector>
// @SupernotePluginExport
// @SupernotePluginAsync
std::vector<std::byte> loadPage(std::int32_t page) {
return readPageBlocking(page);
}JavaScript uses:
const bytes: Uint8Array = await localMath.loadPage(3);For an ordinary blocking C++ function, V2 copies the arguments, registers the Promise operation, and runs the function on the plugin's shared bounded worker executor. Completion returns to the JS thread safely.
SupernotePluginAsync describes the API. The generator does not inspect an
arbitrary std::future or callback type and invent an async adapter for it.
Queue capacity is deliberately bounded. If the plugin accepts work faster than
the executor can hold it, the extra Promise rejects with
RESOURCE_EXHAUSTED instead of allowing an unbounded memory queue.
Initial marked C++ declarations support exactly:
| Meaning | C++ spelling | TypeScript |
|---|---|---|
| No value | void |
void |
| Boolean | bool |
boolean |
| 32-bit integer |
std::int32_t or int32_t
|
number |
| 64-bit integer |
std::int64_t or int64_t
|
bigint |
| 32-bit float | float |
number |
| 64-bit float | double |
number |
| String | std::string |
string |
| Bytes | std::vector<std::byte> |
Uint8Array |
Use these canonical spellings at the marked boundary. Similar-looking aliases, pointers, references, optional values, arbitrary containers, enums, and structs are not accepted automatically.
You can use any types you want inside ordinary implementation code. Convert them at the small marked boundary.
Strings use UTF-8 while crossing native and JNI boundaries. JavaScript itself does not have a special UTF-8 string type.
Bytes are copied values:
JavaScript Uint8Array
-> copy the visible slice
-> std::vector<std::byte>
Only the view's byteOffset and byteLength are passed. Empty arrays work.
null, ArrayBuffer, and other typed arrays are not silently accepted.
Async calls own their copied bytes. Your worker never receives a pointer into JavaScript-owned memory.
Do not pack one number or one point into bytes just to avoid a normal JSI call.
Small scalar calls are appropriate for normal UI events. A packed Uint8Array
is useful when the caller already has a real batch of values and one copy can
replace many separate calls.
For large values, copying can cost much more than the fixed JSI call. Keep the bytes on the side that owns them when possible, avoid echoing a large input back to JavaScript, and return only the result the caller needs.
int64 maps to JavaScript bigint, never number, so IDs, offsets, file sizes,
and timestamps do not silently lose precision.
Use a marked class when JavaScript should keep one native instance and call it more than once.
Put the complete class in a header under the native source root:
// Document.hpp
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
// @SupernotePluginExport
class Document {
public:
explicit Document(std::string path);
// @SupernotePluginExport
std::int32_t pageCount() const;
// @SupernotePluginExport
// @SupernotePluginAsync
std::vector<std::byte> loadPage(std::int32_t page);
void rebuildTemporaryCache(); // ordinary C++, not exposed
private:
std::string path_;
};Keep the implementation in an ordinary .cpp file.
With one eligible public constructor, V2 generates the normal factory automatically:
const document = localMath.Document.create('/path/to/file.note');
const count = document.pageCount();
const bytes = await document.loadPage(0);Marking the class exposes the object type. It does not expose every public method. Each generated method still needs its own export or internal marker.
If a class has multiple eligible public constructors, the generator refuses to guess. Mark the one JavaScript should use:
// @SupernotePluginExport
class Document {
public:
// @SupernoteConstructor
explicit Document(std::string path);
explicit Document(std::int64_t existingHandle);
};Constructors are synchronous; there is no async constructor. If creation needs slow work, keep construction cheap and expose a separate explicitly async method or factory-shaped operation once that design fits your module.
Object parameters and results, returned-only objects, properties, inheritance, and custom factories are not part of the initial object model.
Generated HostObjects use shared ownership. If JavaScript starts an async method and then drops its object reference, the accepted operation keeps the C++ receiver alive until the implementation can no longer access it.
Garbage collection is not cancellation.
Lifetime safety does not make your class thread-safe. These calls may overlap:
document.loadPage(1);
document.loadPage(2);
document.pageCount();The generator does not add a hidden mutex or serial queue around your object. Use a mutex, immutable state, your own serial executor, or another design when the implementation can be accessed concurrently.
Final generated C++ receiver destruction runs later on a managed non-JS context. Do not depend on a particular cleanup thread or exact timing. If a resource must be destroyed on a specific thread, manage that resource inside your implementation.
A synchronous C++ failure is thrown to JavaScript. A failure after an async call has been accepted rejects its Promise.
Unexpected implementation exceptions are exposed as SupernoteError with code
IMPLEMENTATION_ERROR. A broken generated/runtime invariant uses INTERNAL.
The C++ exception class is diagnostic information, not a stable JavaScript API.
Wrong JavaScript argument types throw TypeError, and invalid integer/range
values throw RangeError, before an async operation is created.
See Error Handling for complete synchronous and async
try/catch examples, stable-code handling, and generated internal C++ errors.
A module using the native environment can later add the JVM environment without
conversion. This is useful when native processing also needs Context, Android
storage APIs, or a JVM library.
Read Using Both Environments for independent exports and the generated native-to-JVM internal caller pattern.
After changing marked declarations:
supernote-module update local-math --yes
supernote-module validate local-math --build --verboseThen inspect:
local_modules/local-math/index.d.ts
A local build is not the final JSI test. First load, BigInt, Promise completion, same-process reload, and teardown behavior must be exercised in the intended Supernote and PluginHost environment.