JSI (JavaScript Interface) is the foundational abstraction layer that enables React Native’s new architecture. It provides a C++ interface for JavaScript engines, allowing direct, synchronous communication between JavaScript and native code without the overhead of bridge serialization.
What is JSI?
JSI is defined in ReactCommon/jsi/jsi/jsi.h and provides:
Engine abstraction - Write code that works with any JavaScript engine
Direct interop - JavaScript can call C++ directly and vice versa
Type safety - Strongly typed C++ representations of JavaScript values
Memory efficiency - Zero-copy access to data where possible
Synchronous execution - No async overhead for simple operations
Core Philosophy
JSI separates the “what” from the “how”:
What : Interface for interacting with JavaScript values and executing code
How : Each engine (Hermes, V8, JavaScriptCore) provides its own implementation
This enables React Native to:
Switch JavaScript engines without changing application code
Optimize for different platforms (Hermes for mobile, V8 for desktop)
Support new engines as they emerge
Architecture
Runtime Interface
The jsi::Runtime is the central interface to a JavaScript execution environment:
class JSI_EXPORT Runtime {
public:
// Value creation
virtual Value createNumber ( double value ) = 0 ;
virtual Value createString ( const String & value ) = 0 ;
virtual Value createBool ( bool value ) = 0 ;
virtual Object createObject () = 0 ;
virtual Array createArray ( size_t length ) = 0 ;
// Function execution
virtual Value call (
const Function & func ,
const Value * args ,
size_t count
) = 0 ;
// Property access
virtual Value getProperty (
const Object & obj ,
const PropNameID & name
) = 0 ;
virtual void setProperty (
Object & obj ,
const PropNameID & name ,
const Value & value
) = 0 ;
// Global object
virtual Object global () = 0 ;
// Code evaluation
virtual Value evaluateJavaScript (
const std :: shared_ptr < const Buffer > & buffer ,
const std :: string & sourceURL
) = 0 ;
};
Key principles:
Abstract interface - No engine-specific code
Pure virtual - Each engine provides concrete implementation
Exception safe - Throws jsi::JSError for JavaScript errors
Value Types
JSI provides C++ classes representing JavaScript values:
Primitive Values
class Value {
public:
// Type checking
bool isUndefined () const ;
bool isNull () const ;
bool isBool () const ;
bool isNumber () const ;
bool isString () const ;
bool isObject () const ;
// Value extraction
bool getBool () const ;
double getNumber () const ;
String getString ( Runtime & runtime ) const ;
Object getObject ( Runtime & runtime ) const ;
// Static constructors
static Value undefined ();
static Value null ();
// Move semantics
Value ( Value && other );
Value & operator = ( Value && other );
};
JSI values use move semantics (not copyable) to ensure thread safety and prevent accidental duplication.
String
class String {
public:
// Create from UTF-8
static String createFromUtf8 ( Runtime & runtime , const std :: string & utf8 );
static String createFromAscii ( Runtime & runtime , const char* ascii , size_t length );
// Convert to std::string
std :: string utf8 ( Runtime & runtime ) const ;
// Move-only
String ( String && other );
};
Strings are:
Immutable - Cannot be modified after creation
Reference-counted - Managed by the JavaScript engine
Efficient - May share storage with JavaScript strings
Object
class Object : public Pointer {
public:
// Property access
Value getProperty ( Runtime & runtime , const PropNameID & name ) const ;
void setProperty ( Runtime & runtime , const PropNameID & name , const Value & value );
bool hasProperty ( Runtime & runtime , const PropNameID & name ) const ;
// Array access (if object is array)
Value getPropertyAsArray ( Runtime & runtime , size_t index ) const ;
void setPropertyAsArray ( Runtime & runtime , size_t index , const Value & value );
size_t length ( Runtime & runtime ) const ;
// Type checks
bool isArray ( Runtime & runtime ) const ;
bool isFunction ( Runtime & runtime ) const ;
// Conversion
Array getArray ( Runtime & runtime ) const ;
Function getFunction ( Runtime & runtime ) const ;
};
Array
class Array : public Object {
public:
// Creation
static Array createWithElements (
Runtime & runtime ,
const Value * elements ,
size_t length
);
// Size
size_t size ( Runtime & runtime ) const ;
// Element access
Value getValueAtIndex ( Runtime & runtime , size_t index ) const ;
void setValueAtIndex ( Runtime & runtime , size_t index , const Value & value );
};
Function
class Function : public Object {
public:
// Call function
Value call (
Runtime & runtime ,
const Value * args ,
size_t count
) const ;
Value callWithThis (
Runtime & runtime ,
const Object & thisObj ,
const Value * args ,
size_t count
) const ;
// Create from C++ lambda
static Function createFromHostFunction (
Runtime & runtime ,
const PropNameID & name ,
unsigned int paramCount ,
HostFunctionType func
);
};
using HostFunctionType = std ::function < Value (
Runtime & runtime,
const Value & thisVal,
const Value * args,
size_t count
) > ;
Host Objects
Host objects are C++ objects exposed to JavaScript:
class HostObject {
public:
virtual ~HostObject () = default ;
// Property access from JavaScript
virtual Value get ( Runtime & runtime , const PropNameID & name ) = 0 ;
virtual void set ( Runtime & runtime , const PropNameID & name , const Value & value ) = 0 ;
// Enumerate properties
virtual std :: vector < PropNameID > getPropertyNames ( Runtime & runtime ) = 0 ;
};
Creating a host object:
class MyHostObject : public jsi :: HostObject {
public:
Value get ( Runtime & runtime , const PropNameID & name ) override {
auto propName = name . utf8 (runtime);
if (propName == "value" ) {
return Value (value_);
}
if (propName == "increment" ) {
return Function :: createFromHostFunction (
runtime,
name,
0 , // param count
[ this ]( Runtime & rt , const Value & thisVal , const Value * args , size_t count ) {
value_ ++ ;
return Value :: undefined ();
}
);
}
return Value :: undefined ();
}
void set ( Runtime & runtime , const PropNameID & name , const Value & value ) override {
auto propName = name . utf8 (runtime);
if (propName == "value" ) {
value_ = value . getNumber ();
}
}
std :: vector < PropNameID > getPropertyNames ( Runtime & runtime ) override {
return {
PropNameID :: forAscii (runtime, "value" ),
PropNameID :: forAscii (runtime, "increment" )
};
}
private:
double value_ = 0 ;
};
// Install in JavaScript
auto obj = Object :: createFromHostObject (runtime, std :: make_shared < MyHostObject >());
runtime . global (). setProperty (runtime, "myObject" , obj);
Now in JavaScript:
console . log ( myObject . value ); // 0
myObject . increment ();
console . log ( myObject . value ); // 1
myObject . value = 10 ;
console . log ( myObject . value ); // 10
Host objects power:
TurboModules - Native modules as host objects
Native state - Attach C++ state to JavaScript objects
Custom APIs - Expose any C++ functionality to JavaScript
Native State
Attach C++ objects to JavaScript objects as hidden state:
class MyState : public jsi :: NativeState {
public:
std ::string data;
int count = 0 ;
};
// Attach to JavaScript object
auto jsObject = Object (runtime);
auto state = std :: make_shared < MyState >();
state -> data = "Hello" ;
jsObject . setNativeState (runtime, state);
// Later, retrieve the state
auto retrieved = jsObject . getNativeState < MyState > (runtime);
if (retrieved) {
std ::cout << retrieved -> data << std ::endl; // "Hello"
}
Use cases:
Shadow nodes - Attach C++ shadow nodes to JavaScript fiber nodes
Resource handles - Store native resource handles (file descriptors, etc.)
Cache data - Avoid repeated serialization/deserialization
Threading and Safety
Thread Affinity
JSI values are NOT thread-safe. Each value must only be used on the thread that created it (the JavaScript thread).
Rules:
jsi::Value, jsi::Object, etc. cannot be copied, only moved
Do not store JSI values in data structures accessed from multiple threads
Use RuntimeExecutor to schedule work on the JavaScript thread
RuntimeExecutor
Safely access the runtime from any thread:
using RuntimeExecutor = std ::function < void (
std ::function < void ( jsi ::Runtime & runtime) >&& callback
) > ;
// From any thread
runtimeExecutor ([ data ]( jsi :: Runtime & runtime ) {
// This executes on JavaScript thread
auto jsString = jsi :: String :: createFromUtf8 (runtime, data);
auto global = runtime . global ();
auto callback = global . getPropertyAsFunction (runtime, "onData" );
callback . call (runtime, jsString);
});
Thread-Safe Patterns
Sharing Data to JavaScript
// Background thread
void backgroundWork () {
auto result = performExpensiveComputation ();
// Capture by value to avoid lifetime issues
runtimeExecutor_ ([ result ]( jsi :: Runtime & runtime ) {
// Now on JavaScript thread, safe to create JSI values
auto jsResult = jsi :: Value (runtime, result);
notifyJavaScript (runtime, jsResult);
});
}
Sharing Data from JavaScript
Value fetchData ( Runtime & runtime , const Value * args , size_t count ) {
auto url = args [ 0 ]. getString (runtime). utf8 (runtime);
auto promise = Promise :: createWithResolver (runtime);
// Capture resolver, move to background thread
auto resolver = promise . getResolver (runtime);
std :: thread ([ url , resolver , jsInvoker = jsInvoker_ ]() {
auto result = performNetworkRequest (url);
jsInvoker -> invokeAsync ([ result , resolver ]( Runtime & runtime ) {
resolver . resolve (runtime, jsi :: String :: createFromUtf8 (runtime, result));
});
}). detach ();
return promise . getPromise (runtime);
}
JSI Decorators
Decorators wrap a runtime to add functionality. Defined in jsi/decorator.h:
class RuntimeDecorator : public Runtime {
public:
RuntimeDecorator ( Runtime & runtime ) : runtime_ (runtime) {}
// Delegate all operations to wrapped runtime
Value createNumber ( double value ) override {
return runtime_ . createNumber (value);
}
// Can intercept and modify behavior
Value call ( const Function & func , const Value * args , size_t count ) override {
// Log the call
logFunctionCall (func, args, count);
// Delegate to wrapped runtime
return runtime_ . call (func, args, count);
}
protected:
Runtime & runtime_;
};
Use cases:
Profiling - Time function calls, memory allocations
Debugging - Log all JavaScript operations
Security - Restrict access to certain APIs
Testing - Mock or stub JavaScript behavior
Instrumentation
JSI provides instrumentation hooks defined in jsi/instrumentation.h:
class Instrumentation {
public:
// String creation
virtual void onStringCreated ( const String & str ) {}
// Object lifecycle
virtual void onObjectCreated ( const Object & obj ) {}
virtual void onObjectDestroyed ( const Object & obj ) {}
// Function calls
virtual void onFunctionCalled ( const Function & func ) {}
// Garbage collection
virtual void onGarbageCollectionStart () {}
virtual void onGarbageCollectionEnd () {}
};
Enables:
Memory profiling - Track object allocations
Performance monitoring - Measure function call overhead
Leak detection - Find objects not being released
React DevTools - Power debugging features
Error Handling
JSError
JavaScript errors are represented as jsi::JSError:
try {
auto func = runtime . global (). getPropertyAsFunction (runtime, "maybeThrows" );
auto result = func . call (runtime);
} catch ( const jsi ::JSError & error) {
// Access error details
std ::string message = error . getMessage ();
std ::string stack = error . getStack ();
// Get original JavaScript error object
jsi ::Value errorValue = error . value ();
std ::cerr << "JavaScript error: " << message << std ::endl;
std ::cerr << "Stack trace: " << stack << std ::endl;
}
Throwing Errors to JavaScript
Value riskyOperation ( Runtime & runtime , const Value * args , size_t count ) {
if (count == 0 ) {
throw jsi :: JSError (runtime, "Expected at least one argument" );
}
if ( ! args [ 0 ]. isString ()) {
throw jsi :: JSError (
runtime,
jsi :: String :: createFromUtf8 (runtime, "Argument must be a string" )
);
}
// ... perform operation
}
In JavaScript:
try {
nativeModule . riskyOperation ();
} catch ( error ) {
console . log ( error . message ); // "Expected at least one argument"
}
Memory Management
Automatic Memory Management
JSI objects are automatically managed:
JavaScript engine handles garbage collection
C++ smart pointers manage native objects
No manual reference counting needed
Move Semantics
JSI values use move semantics for safety:
// Good: Move value
jsi ::Value value1 = createValue ();
jsi ::Value value2 = std :: move (value1);
// value1 is now invalid, value2 owns the value
// Bad: Cannot copy
jsi ::Value value3 = value2; // Compile error!
Benefits:
Thread safety - Cannot accidentally share across threads
Clear ownership - Ownership transfer is explicit
Performance - No unnecessary copies
Weak References
Prevent reference cycles:
class Observer : public jsi :: HostObject {
public:
void setTarget ( Runtime & runtime , const Object & target ) {
// Store weak reference
target_ = jsi :: WeakObject (runtime, target);
}
Value notify ( Runtime & runtime ) {
// Try to lock weak reference
auto target = target_ . lock (runtime);
if ( target . isUndefined ()) {
// Target was garbage collected
return Value :: undefined ();
}
// Target still alive, use it
auto func = target . getObject (runtime). getPropertyAsFunction (runtime, "onNotify" );
return func . call (runtime);
}
private:
jsi ::WeakObject target_;
};
Integration with JavaScript Engines
Hermes
Facebook’s JavaScript engine optimized for React Native:
#include <hermes/hermes.h>
std ::unique_ptr < jsi ::Runtime > runtime = facebook :: hermes :: makeHermesRuntime ();
Hermes features:
Bytecode compilation - Precompile JavaScript to bytecode
Efficient memory - Optimized for mobile devices
Fast startup - Faster than JavaScriptCore on mobile
JavaScriptCore
Apple’s JavaScript engine:
#include <jsi/JSCRuntime.h>
std ::unique_ptr < jsi ::Runtime > runtime = facebook :: jsc :: makeJSCRuntime ();
Used on iOS when Hermes is not enabled.
Google’s JavaScript engine:
#include <jsi/V8Runtime.h>
std ::unique_ptr < jsi ::Runtime > runtime = facebook :: v8 :: makeV8Runtime ();
Optional, used in some React Native platforms.
Practical Examples
Calling JavaScript from C++
void notifyJavaScript ( Runtime & runtime , const std :: string & message ) {
// Get global function
auto global = runtime . global ();
auto console = global . getPropertyAsObject (runtime, "console" );
auto log = console . getPropertyAsFunction (runtime, "log" );
// Call console.log
auto jsMessage = jsi :: String :: createFromUtf8 (runtime, message);
log . call (runtime, jsMessage);
}
Calling C++ from JavaScript
void installNativeFunction ( Runtime & runtime ) {
auto func = Function :: createFromHostFunction (
runtime,
PropNameID :: forAscii (runtime, "nativeGreet" ),
1 , // param count
[]( Runtime & runtime , const Value & thisVal , const Value * args , size_t count ) {
auto name = args [ 0 ]. getString (runtime). utf8 (runtime);
auto greeting = "Hello, " + name + "!" ;
return jsi :: String :: createFromUtf8 (runtime, greeting);
}
);
runtime . global (). setProperty (runtime, "nativeGreet" , func);
}
JavaScript:
const greeting = nativeGreet ( "World" );
console . log ( greeting ); // "Hello, World!"
Returning Promises
Value asyncOperation ( Runtime & runtime , const Value * args , size_t count ) {
// Create Promise
auto promise = Promise :: create (runtime, []( Runtime & runtime ,
std :: shared_ptr < Promise :: Resolver > resolver ) {
// Simulate async work
std :: thread ([ resolver ]( Runtime & runtime ) {
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 1 ));
// Resolve on JavaScript thread
resolver -> resolve (runtime, jsi :: String :: createFromUtf8 (runtime, "Done!" ));
}). detach ();
});
return promise;
}
JavaScript:
const result = await asyncOperation ();
console . log ( result ); // "Done!" (after 1 second)
Working with Arrays
Value sumArray ( Runtime & runtime , const Value * args , size_t count ) {
auto arr = args [ 0 ]. getObject (runtime). getArray (runtime);
auto length = arr . size (runtime);
double sum = 0 ;
for ( size_t i = 0 ; i < length; i ++ ) {
auto element = arr . getValueAtIndex (runtime, i);
if ( element . isNumber ()) {
sum += element . getNumber ();
}
}
return Value (sum);
}
Working with Objects
Value getProperty ( Runtime & runtime , const Value * args , size_t count ) {
auto obj = args [ 0 ]. getObject (runtime);
auto key = args [ 1 ]. getString (runtime). utf8 (runtime);
auto propName = PropNameID :: forUtf8 (runtime, key);
if ( obj . hasProperty (runtime, propName)) {
return obj . getProperty (runtime, propName);
}
return Value :: undefined ();
}
Minimize String Conversions
// Bad: Multiple conversions
void processNames ( Runtime & runtime , const Array & names ) {
for ( size_t i = 0 ; i < names . size (runtime); i ++ ) {
auto name = names . getValueAtIndex (runtime, i). getString (runtime);
auto str = name . utf8 (runtime); // Expensive!
process (str);
}
}
// Good: Keep in JSI format when possible
void processNames ( Runtime & runtime , const Array & names ) {
for ( size_t i = 0 ; i < names . size (runtime); i ++ ) {
auto name = names . getValueAtIndex (runtime, i). getString (runtime);
processJSI (runtime, name); // Work with jsi::String directly
}
}
Cache Property Names
class MyModule {
public:
MyModule ( Runtime & runtime )
: propName_ ( PropNameID :: forAscii (runtime, "myProperty" )) {}
Value getProperty ( Runtime & runtime , const Object & obj ) {
// Reuse cached PropNameID
return obj . getProperty (runtime, propName_);
}
private:
PropNameID propName_;
};
Batch Operations
// Bad: Multiple JSI calls in loop
for ( int i = 0 ; i < 1000 ; i ++ ) {
callJavaScript (runtime, i);
}
// Good: Batch into single call
Auto arr = Array (runtime, 1000 );
for ( int i = 0 ; i < 1000 ; i ++ ) {
arr . setValueAtIndex (runtime, i, Value (i));
}
callJavaScript (runtime, arr);
Debugging JSI
Logging
void logValue ( Runtime & runtime , const Value & value ) {
if ( value . isUndefined ()) {
std ::cout << "undefined" << std ::endl;
} else if ( value . isNull ()) {
std ::cout << "null" << std ::endl;
} else if ( value . isBool ()) {
std ::cout << ( value . getBool () ? "true" : "false" ) << std ::endl;
} else if ( value . isNumber ()) {
std ::cout << value . getNumber () << std ::endl;
} else if ( value . isString ()) {
std ::cout << value . getString (runtime). utf8 (runtime) << std ::endl;
} else if ( value . isObject ()) {
std ::cout << "[object]" << std ::endl;
}
}
LLDB/GDB Debugging
Set breakpoints in native code:
# LLDB
( lldb ) b MyHostObject::get
( lldb ) run
# When hit, inspect JSI values
( lldb ) p name.utf8 ( runtime )
Xcode Instruments
Time Profiler - Find slow JSI operations
Allocations - Track JSI object creation
Leaks - Find unreleased JSI objects
Further Reading
TurboModules See JSI in action with TurboModules
Fabric Renderer How Fabric uses JSI for rendering
Architecture Overview JSI’s role in React Native architecture
Threading Model Thread safety with JSI