Skip to main content
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.

V8

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();
}

Performance Considerations

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

Build docs developers (and LLMs) love