DEV Community

Rahul Garg
Rahul Garg

Posted on

The 5 C++ Concepts Every React Native Developer Needs (and Nothing More)

"The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise." — Edsger W. Dijkstra, The Humble Programmer, 1972

Excerpt: You don't need to learn all of C++ to write JSI native modules. You need five concepts: stack vs heap, references and pointers, RAII, smart pointers, and lambdas. This post teaches exactly that subset — framed in JavaScript terms you already know. By the end, you'll read C++ the way you read TypeScript: not every keyword, but every intention.

Series: React Native JSI Deep Dive (12 parts — series in progress) Part 1: React Native Architecture — Threads, Hermes, and the Event Loop | Part 2: React Native Bridge vs JSI — What Changed and Why | Part 3: C++ for JavaScript Developers (You are here) | Part 4: Your First React Native JSI Function | Part 5: HostObjects — Exposing C++ Classes to JavaScript | Part 6: Memory Ownership | Part 7: Platform Wiring | Part 8: Threading & Async | Part 9: Real-Time Audio in React Native — Lock-Free Pipelines with JSI | Part 10: Storage Engine | Part 11: Module Approaches | Part 12: Debugging


The Problem: C++ Looks Like It Was Designed to Cause Suffering

If you've been writing JavaScript or TypeScript, your first encounter with C++ probably looks like this:

What a JSI function looks like

static jsi::Value multiply(
    jsi::Runtime& rt,
    const jsi::Value& thisVal,
    const jsi::Value* args,
    size_t count) {
  double a = args[0].asNumber();
  double b = args[1].asNumber();
  return jsi::Value(a * b);
}

You see &, *, const, size_t, and you think: I have to learn an entirely new language. But look again. Strip the symbols and it's a function that takes two numbers and returns their product. The & and * are about one thing only: who owns the data and where it lives.

That's the entire mental shift. JavaScript hides memory management behind a garbage collector. C++ makes you state it explicitly. Everything else — classes, loops, conditionals, strings — works roughly the way you'd expect.

This post teaches you the five C++ concepts that appear in every JSI module. No templates-of-templates. No operator overloading. No multiple inheritance. Just the vocabulary you need to read and write native module code.


Concept 1: Stack vs Heap (Where Data Lives)

In JavaScript, you never think about where your variables live. You write const x = 42 and the engine figures out the rest.

In C++, data primarily lives in one of two places — the stack or the heap — and you choose which one. (C++ also has static storage for globals and thread-local storage, but for JSI modules, stack and heap are what matter.)

The Stack

The stack is fast, automatic memory. When a function runs, its local variables live on the stack. When the function returns, they're destroyed. No cleanup required — it's instant.

Stack allocation — automatic lifetime

void greet() {
    int count = 42;            // lives on the stack
    std::string name = "JSI";  // also on the stack (string content may be heap-allocated internally)
    // use count and name...
}  // ← count and name are destroyed here, automatically

The JavaScript equivalent is a let inside a function — it exists while the function runs and is eligible for garbage collection after. But there's a crucial difference: in C++, stack destruction is immediate and deterministic. It doesn't happen "eventually" when a GC gets around to it. It happens at the closing brace. Every time. Guaranteed.

The Heap

The heap is for data that needs to outlive the function that created it. In JavaScript, everything that isn't a primitive (number, boolean) lives on the heap — objects, arrays, strings, closures. The garbage collector handles cleanup.

In C++, you explicitly allocate on the heap with new and must explicitly deallocate with delete:

Heap allocation — manual lifetime ⚠️

void createBuffer() {
    int* data = new int[1024];  // allocate 1024 ints on the heap
    // use data...
    delete[] data;              // YOU must free it
}  // if you forget delete[], those 1024 ints leak forever

Think about it: What happens if an exception is thrown between new and delete? The delete never runs. The memory leaks. This is the fundamental problem with manual memory management — and it's why modern C++ almost never uses raw new and delete. The solution is RAII, which we'll get to in Concept 3.

Here's the mental model:

┌─────────────────────────────────────────────────────────┐
│                        STACK                             │
│                                                          │
│  Fast. Automatic. Fixed-size.                            │
│  Dies when the function returns.                         │
│                                                          │
│  JS analogy: primitive values (number, boolean)          │
│  C++ use: local variables, function arguments            │
│                                                          │
├─────────────────────────────────────────────────────────┤
│                        HEAP                              │
│                                                          │
│  Slower. Manual (or smart-pointer managed). Dynamic.     │
│  Lives until you explicitly free it — or it leaks.       │
│                                                          │
│  JS analogy: objects, arrays, closures (GC'd)            │
│  C++ use: anything that outlives its creating function    │
│                                                          │
└─────────────────────────────────────────────────────────┘

Figure 1: Stack vs heap. JavaScript hides this distinction behind the garbage collector. C++ requires you to choose.

For JSI modules, you'll mostly work with stack-allocated values and smart pointers (which manage heap memory for you). Raw new and delete almost never appear in well-written modern C++.


Concept 2: References and Pointers (Aliasing Data)

In JavaScript, when you pass an object to a function, the function receives a copy of the reference — it can mutate the object's properties, but reassigning the parameter doesn't affect the caller's variable. (This is technically "pass-by-sharing," not true pass-by-reference in the C++ sense.) In practice, it feels like pass-by-reference for mutations:

JavaScript — object mutations are visible to the caller

function addItem(list) {
    list.push('new item');  // modifies the original
}

const myList = ['a', 'b'];
addItem(myList);
console.log(myList);  // ['a', 'b', 'new item']

In C++, you choose whether to pass data by value (copy it), by reference (alias it), or by pointer (hold its address). This is what the & and * symbols mean.

Pass by Value (Copy)

Pass by value — makes a copy

void process(std::string text) {   // text is a COPY
    text += " modified";           // modifies the copy only
}

std::string original = "hello";
process(original);
// original is still "hello" — the copy was modified, not the original

This is like JavaScript's behavior with primitives — let x = 5; foo(x); passes a copy.

Pass by Reference (`&`)

Pass by reference — aliases the original

void process(std::string& text) {  // & means "reference to"
    text += " modified";           // modifies the ORIGINAL
}

std::string original = "hello";
process(original);
// original is now "hello modified"

The & after the type means "this is not a copy — it's another name for the same data." The closest JavaScript analogy is passing an object to a function — the function can mutate the object's properties because it has a reference to the same data. But C++ references go further: reassigning the parameter does affect the caller's variable, unlike JavaScript.

Const Reference (`const &`)

Const reference — read-only alias

void print(const std::string& text) {  // can read but not modify
    std::cout << text;                  // OK — reading
    // text += " nope";                // COMPILER ERROR — can't modify
}

This is the most common pattern in JSI code. When a function receives data it needs to read but not modify, it takes a const &. It avoids the cost of copying while preventing accidental mutation.

Key Insight: When you see const jsi::Value& in a JSI function signature, read it as: "I'm borrowing this value for the duration of this call. I won't modify it, and I won't keep it after I return." The const is a promise to the compiler — and to every developer who reads your code.

Pointers (`*`)

A pointer holds the memory address of data. It's a lower-level construct than a reference — references are typically implemented as pointers under the hood, but with safer semantics (can't be null, can't be reseated).

Pointers — the address operator

int value = 42;
int* ptr = &value;    // ptr holds the ADDRESS of value
                      // (& here means "address of", not "reference" — context matters)
std::cout << *ptr;    // 42 — *ptr DEREFERENCES the pointer (reads the value at that address)

You'll see pointers in JSI function signatures:

JSI function — pointer to argument array

jsi::Value myFunction(
    jsi::Runtime& rt,          // reference to the runtime
    const jsi::Value& thisVal, // const reference to "this"
    const jsi::Value* args,    // POINTER to array of arguments
    size_t count               // how many arguments
) {
    double x = args[0].asNumber();  // args[0] works like array indexing
    // ...
}

The args parameter is a pointer to the first element of an array. args[0] is the first argument, args[1] is the second. The count parameter tells you how many there are. This is C-style array passing — no .length property, so the count is passed separately.

The Cheat Sheet

Symbol Meaning JS Analogy
`Type x` Value (copy) Primitive: `let x = 5`
`Type& x` Reference (alias) Object parameter: `function f(obj)`
`const Type& x` Read-only reference A read-only view — others may still have write access
`Type* x` Pointer (memory address) No direct analogy — closest is a weak reference
`&x` "Address of x" (in an expression) No analogy
`*x` "Value at address x" (dereference) No analogy

Figure 2: C++ parameter passing symbols. The & does double duty — in a type declaration it means "reference," in an expression it means "address of."

Gotcha: The & symbol has two completely different meanings depending on context. In a type declaration (std::string& text), it means "reference to." In an expression (int* ptr = &value), it means "address of." This trips up every JS developer learning C++. When you see &, check whether it's next to a type or next to a variable name.


Concept 3: RAII (The Destroyer Pattern)

RAII — Resource Acquisition Is Initialization — is the most important C++ concept for JSI development. It has the worst name in computer science, but the idea is simple.

In JavaScript, you write cleanup code manually:

JavaScript — manual cleanup with try/finally

function readFile(path) {
    const handle = openFile(path);
    try {
        return handle.read();
    } finally {
        handle.close();  // you must remember this
    }
}

If you forget the finally, the file handle leaks. If an exception throws before close() but outside the try, the file handle leaks. It's fragile.

In C++, RAII means: the constructor acquires the resource, and the destructor releases it. Since destructors run automatically when objects leave scope (stack unwinding), cleanup is guaranteed — even if exceptions are thrown.

C++ — RAII makes cleanup automatic

class FileHandle {
    FILE* file_;
public:
    FileHandle(const char* path) : file_(fopen(path, "r")) {   // acquire
        if (!file_) throw std::runtime_error("Failed to open file");
    }
    ~FileHandle() { fclose(file_); }                             // release — runs automatically

    std::string read() { /* ... */ }
};

void readFile(const char* path) {
    FileHandle handle(path);     // constructor opens the file
    auto content = handle.read();
    return content;
}  // ← destructor runs here — file is closed, guaranteed
   //   even if handle.read() threw an exception

The ~FileHandle() is a destructor — a function that runs automatically when the object is destroyed. For stack-allocated objects, that means when the scope ends (the closing }). For heap-allocated objects, it means when delete is called (or when a smart pointer decides it's time).

Key Insight: RAII is not about files. It's about any resource — memory, network connections, locks, GPU buffers, audio sessions. The pattern is always the same: acquire in the constructor, release in the destructor, and let scope determine lifetime. In JSI modules, HostObjects use RAII for their C++ state: when JavaScript's garbage collector collects the HostObject, the C++ destructor runs and cleans up native resources.

Here's the mental model:

JavaScript:                          C++ (RAII):

  const x = acquire();                {
  try {                                 Resource x(...);  // acquire
    use(x);                             use(x);
  } finally {                        }  // ← destructor releases
    release(x);                         //   automatically, even
  }                                     //   on exception

Figure 3: RAII eliminates manual cleanup. The closing brace IS the finally block.

The reason RAII matters for JSI: native modules manage resources that the JavaScript garbage collector knows nothing about — audio buffers, file handles, database connections, native thread pools. RAII ensures these resources are cleaned up deterministically, not "whenever the GC gets around to it."


Concept 4: Smart Pointers (Automatic Heap Management)

Raw new and delete are C++'s type-safe, object-aware replacements for C's malloc and free. Unlike malloc/free, new calls constructors and delete calls destructors — but they're still error-prone when used manually. Modern C++ uses smart pointers — RAII wrappers around heap pointers that automatically delete the memory when it's no longer needed.

There are two smart pointers you need to know. Think of them as two different ownership policies.

`std::unique_ptr` — Exclusive Ownership

A unique_ptr owns its data exclusively. Nobody else can own it. When the unique_ptr is destroyed, the data is freed. You cannot copy it — you can only move it (transfer ownership).

unique_ptr — one owner, automatic cleanup

#include <memory>

void example() {
    // Create a unique_ptr — it owns the AudioBuffer
    auto buffer = std::make_unique<AudioBuffer>(1024);
    buffer->fill(0.0f);    // use it like a regular pointer

    // auto copy = buffer;  // ❌ COMPILER ERROR — can't copy a unique_ptr
    auto moved = std::move(buffer);  // ✓ Transfer ownership
    // buffer is now nullptr — moved owns the data
}  // ← moved is destroyed here, AudioBuffer is freed

The JavaScript analogy: imagine a const reference that you can't share. Only one variable can point to the data at a time. If you want to pass it somewhere else, you move it — the original becomes null.

unique_ptr ownership:

  auto a = make_unique<X>();     a ──────▶ [X on heap]

  auto b = std::move(a);         a ──▶ nullptr
                                 b ──────▶ [X on heap]

  // b goes out of scope          b destroyed → [X freed]

Figure 4: unique_ptr ownership transfer. Only one pointer to the data at a time. Moving transfers ownership and nullifies the source.

`std::shared_ptr` — Shared Ownership

A shared_ptr lets multiple owners share the same data. It maintains a reference count — every copy increments the count, every destruction decrements it. When the count hits zero, the data is freed.

shared_ptr — reference-counted ownership

#include <memory>

void example() {
    auto config = std::make_shared<AppConfig>();  // refcount = 1

    auto copy1 = config;   // refcount = 2 (both point to same AppConfig)
    auto copy2 = config;   // refcount = 3

    copy1.reset();          // refcount = 2 (copy1 releases its share)
    copy2.reset();          // refcount = 1
}  // config destroyed → refcount = 0 → AppConfig freed

This is closest to how JavaScript's garbage collector works — the object lives as long as someone references it. The difference: shared_ptr uses deterministic reference counting (freed immediately when count hits zero), while JS uses tracing GC (freed "eventually" during a GC pass).

shared_ptr reference counting:

  auto a = make_shared<X>();     a ──────▶ [X] refcount: 1
  auto b = a;                    a ──────▶ [X] refcount: 2
                                 b ──────┘
  a.reset();                     b ──────▶ [X] refcount: 1
  b.reset();                               [X] refcount: 0 → freed

Figure 5: shared_ptr reference counting. Multiple pointers to the same data. Freed when the last one lets go.

Which One for JSI?

Smart Pointer Use When JSI Example
`unique_ptr` One owner, no sharing needed Internal buffers, temporary computation results
`shared_ptr` Multiple owners, or exposed to JS **HostObjects** — JS GC and C++ code both need access

The critical one for JSI is shared_ptr. When you create a HostObject (a C++ object exposed to JavaScript), it's wrapped in a std::shared_ptr. The JavaScript garbage collector holds one reference, and your C++ code may hold others. The HostObject is destroyed only when both JS and C++ have released their references.

HostObject uses shared_ptr — preview of Part 5

// HostObjects are always shared_ptr — JS GC holds one reference
auto storage = std::make_shared<StorageHostObject>(dbPath);
runtime.global().setProperty(
    runtime, "storage",
    jsi::Object::createFromHostObject(runtime, storage)
);
// Now: JS holds a reference (via GC) AND C++ holds 'storage'
// StorageHostObject is freed when BOTH release

Gotcha: shared_ptr has overhead — the reference count is an atomic integer (thread-safe increment/decrement), and each shared_ptr is larger than a raw pointer (it carries the control block). For hot paths and real-time code, prefer unique_ptr. We'll see why this matters in Parts 8 and 9 when we build threaded and audio pipeline code.


Concept 5: Lambdas (C++ Closures)

Lambdas are the C++ concept you'll recognize most immediately. They're closures — anonymous functions that can capture variables from their surrounding scope.

JavaScript closure

function makeCounter() {
    let count = 0;
    return () => ++count;
}

C++ lambda — the same pattern

auto makeCounter() {
    int count = 0;
    return [count]() mutable { return ++count; };
}

The syntax looks different, but the observable output is the same: call the returned function three times and you get 1, 2, 3. The mechanism differs — JS captures the variable binding (shared with other closures from the same scope), while C++ [count] mutable captures a private copy — but for a single returned counter, the result is identical.

Lambda Anatomy

[capture](parameters) -> return_type { body }

The capture list [...] is what makes C++ lambdas different from JavaScript closures. In JavaScript, closures automatically capture the enclosing scope's variable bindings — they see mutations to those variables, similar in behavior to C++ capture-by-reference (though JS keeps the scope alive via GC, so there's no dangling reference risk). In C++, you explicitly choose what to capture and how:

Capture modes

int x = 10;
std::string name = "JSI";

auto byValue   = [x]()       { return x; };          // copies x (snapshot)
auto byRef     = [&x]()      { return x; };          // references x (live alias)
auto everything = [=]()      { return x; };           // copies ALL variables
auto allByRef  = [&]()       { return x; };           // references ALL variables
auto mixed     = [x, &name]() { return name + "!"; }; // x by value, name by ref
Capture Syntax JS Analogy Behavior
By value `[x]` `const x_copy = x` then use `x_copy` Snapshot — changes to `x` outside don't affect the lambda
By reference `[&x]` Closest to JS closure behavior Live alias — sees changes to `x`, and can modify it
All by value `[=]` No direct analogy Copies everything referenced in the body
All by reference `[&]` Closest to default JS closures References everything — most like JavaScript

Figure 6: Lambda capture modes. JavaScript closures always share the enclosing scope's bindings. C++ makes you choose — and the choice matters for thread safety.

Why Captures Matter for JSI

This is where the JSI connection becomes critical. When you create a JSI host function, you typically use a lambda:

JSI host function with lambda capture

void install(jsi::Runtime& runtime, std::shared_ptr<Database> db) {
    auto get = jsi::Function::createFromHostFunction(
        runtime,
        jsi::PropNameID::forAscii(runtime, "get"),
        1,  // argument count
        [db](jsi::Runtime& rt,              // ← capture db by value (shared_ptr copy)
             const jsi::Value& thisVal,
             const jsi::Value* args,
             size_t count) -> jsi::Value {
            auto key = args[0].asString(rt).utf8(rt);
            auto result = db->get(key);     // use the captured database
            return jsi::String::createFromUtf8(rt, result);
        }
    );
    runtime.global().setProperty(runtime, "dbGet", std::move(get));
}

Notice: the lambda captures db by value — but db is a shared_ptr, so capturing by value copies the shared pointer, incrementing the reference count. The lambda now shares ownership of the database. Even if the original db variable goes out of scope, the lambda's copy keeps the database alive.

Think about it: What would happen if we captured db by reference ([&db]) instead of by value ([db])? The install function would return, db (a local variable) would be destroyed, and the lambda would hold a dangling reference — a pointer to memory that no longer exists. The next time JavaScript called dbGet(), it would crash. This is why JSI lambdas almost always capture shared_ptr by value, not by reference.

This pattern — capturing shared_ptr by value inside JSI lambdas — appears in virtually every native module. It's how C++ objects stay alive as long as JavaScript needs them.


Move Semantics: Transferring Ownership

One more concept ties everything together. You've already seen std::move with unique_ptr. Let's understand what it actually does.

In JavaScript, assigning an object doesn't copy it:

JavaScript — objects are shared, not copied

const a = { data: [1, 2, 3] };
const b = a;      // b and a point to the SAME object
b.data.push(4);   // a.data is also [1, 2, 3, 4]

In C++, assigning an object copies it by default:

C++ — objects are copied by default

std::vector<int> a = {1, 2, 3};
std::vector<int> b = a;     // b is a COPY — a and b are independent
b.push_back(4);             // a is still {1, 2, 3}

Copying is safe but expensive. If a holds a megabyte of data, b = a copies that megabyte. std::move says: "I'm done with a — transfer its guts to b without copying."

Move — transfer without copying

std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);  // b steals a's internal buffer
// a is now in a "moved-from" state — valid but unspecified (typically empty)
// b holds {1, 2, 3} — no copy happened

Think of it like this: a normal copy is photocopying a 100-page document. A move is handing someone the document — instant, but now you don't have it anymore.

Copy:    a ──▶ [1,2,3]         b ──▶ [1,2,3]    (two copies exist)

Move:    a ──▶ []              b ──▶ [1,2,3]    (data transferred, no copy)

Figure 7: Copy vs move. Copy duplicates the data. Move transfers ownership — the source is left in a valid but unspecified state (typically empty).

You'll see std::move in JSI code when:

  • Transferring a unique_ptr to a new owner
  • Passing a large object into a function without copying it
  • Returning a constructed object from a function efficiently

Move in JSI context

// Moving a JSI function into a property (no copy needed)
auto fn = jsi::Function::createFromHostFunction(rt, name, 0, callback);
runtime.global().setProperty(runtime, "myFunc", std::move(fn));
// fn is now empty — runtime.global() owns the function

Putting It All Together: Reading Real JSI Code

Let's apply all five concepts to a real-world JSI module. This is a simplified version of what you'd see in a library like react-native-mmkv:

A complete mini JSI module — every concept in action

#include <jsi/jsi.h>
#include <memory>
#include <string>
#include <unordered_map>

using namespace facebook;

// A simple in-memory key-value store
class KeyValueStore {                                    // RAII: constructor acquires,
public:                                                  //       destructor releases
    void set(const std::string& key,                     // const& — read-only reference
             const std::string& value) {
        data_[key] = value;
    }

    std::string get(const std::string& key) const {      // const method — won't modify state
        auto it = data_.find(key);
        if (it != data_.end()) return it->second;
        return "";
    }

private:
    std::unordered_map<std::string, std::string> data_;  // stack-allocated (inside the object)
};  // destructor frees data_ automatically (RAII)

void installStorage(jsi::Runtime& rt) {                  // reference — aliases the runtime
    // shared_ptr: JS GC and C++ both need access
    auto store = std::make_shared<KeyValueStore>();

    // "set" function — lambda captures store by value (shared_ptr copy, refcount++)
    auto setFn = jsi::Function::createFromHostFunction(
        rt, jsi::PropNameID::forAscii(rt, "set"), 2,
        [store](jsi::Runtime& rt, const jsi::Value&,
                const jsi::Value* args, size_t count) -> jsi::Value {
            auto key = args[0].asString(rt).utf8(rt);    // jsi::String → std::string
            auto val = args[1].asString(rt).utf8(rt);
            store->set(key, val);                        // use captured shared_ptr
            return jsi::Value::undefined();
        }
    );

    // "get" function — same capture pattern
    auto getFn = jsi::Function::createFromHostFunction(
        rt, jsi::PropNameID::forAscii(rt, "get"), 1,
        [store](jsi::Runtime& rt, const jsi::Value&,
                const jsi::Value* args, size_t count) -> jsi::Value {
            auto key = args[0].asString(rt).utf8(rt);
            auto result = store->get(key);
            return jsi::String::createFromUtf8(rt, result);
        }
    );

    // Install into JavaScript global scope — move (no copy needed)
    auto storage = jsi::Object(rt);
    storage.setProperty(rt, "set", std::move(setFn));    // move: transfer ownership
    storage.setProperty(rt, "get", std::move(getFn));
    rt.global().setProperty(rt, "storage", std::move(storage));
}

Using it from JavaScript

storage.set('theme', 'dark');                // synchronous — no await
const theme = storage.get('theme');          // synchronous — returns immediately
console.log(theme);                          // "dark"

output

"dark"

Every concept from this post appears in that code:

Line Concept What's Happening
`jsi::Runtime& rt` **Reference** Borrows the runtime — doesn't own it
`const jsi::Value&` **Const reference** Read-only access to the `this` value
`const jsi::Value* args` **Pointer** Points to the argument array
`std::make_shared()` **Smart pointer** Heap allocation with shared ownership
`[store](...) { ... }` **Lambda + capture** Closure capturing `shared_ptr` by value
`std::move(setFn)` **Move** Transfers function ownership to the object
`~KeyValueStore()` (implicit) **RAII** Destructor frees `data_` when store is destroyed

The Concepts You Don't Need (Yet)

C++ is enormous. Here's what you can safely ignore for JSI work:

C++ Feature Why You Don't Need It
Templates (advanced) JSI uses them internally, but you rarely write them
Multiple inheritance JSI uses single inheritance (`HostObject` base class)
Operator overloading (advanced) JSI uses move-assignment operators and basic comparisons internally, but you won't write custom overloads for JSI modules
`const_cast` / `reinterpret_cast` Have legitimate uses in systems code, but you won't need them for JSI modules
Manual `new` / `delete` Use `make_unique` and `make_shared` instead
Preprocessor macros (`#define`) Occasionally for platform `#ifdef`, but not for logic

If you encounter these in third-party native modules, you can usually understand the surrounding code without understanding the advanced feature.


Key Takeaways

  • Stack vs heap. Stack memory is automatic — allocated when a function starts, freed when it returns. Heap memory outlives functions but must be managed. For JSI modules, smart pointers manage the heap for you.

  • References (&) and pointers (*). References are aliases — another name for existing data. const & means "read-only borrow." Pointers hold memory addresses. In JSI code, you'll see jsi::Runtime& (borrow the runtime) and const jsi::Value* (pointer to the argument array).

  • RAII. Constructors acquire resources, destructors release them. Scope determines lifetime. This is C++'s answer to try/finally — but it's built into the language and can't be forgotten. Every HostObject relies on RAII to clean up native resources when JavaScript garbage-collects it.

  • Smart pointers. unique_ptr = one owner, automatic cleanup. shared_ptr = shared ownership via reference counting. HostObjects use shared_ptr because both JavaScript's GC and C++ code need to hold references to the same object.

  • Lambdas capture explicitly. Unlike JavaScript closures (which automatically share the enclosing scope's variable bindings), C++ lambdas require you to declare what they capture and how. For JSI, the key pattern is: capture shared_ptr by value inside lambdas so the native object stays alive as long as JavaScript needs it.


What's Next

You now have the C++ vocabulary. You know where data lives (stack vs heap), how to borrow it (&), how to manage it (unique_ptr, shared_ptr), how to clean it up (RAII), and how to write closures (lambdas with explicit captures).

In Part 4: Your First JSI Function, we put it all together. You'll write a JSI function from scratch — registering it with the runtime, validating arguments, handling errors, and calling it from JavaScript. No boilerplate generators, no codegen. Just raw JSI.

Part 3 gave you the vocabulary. Part 4 gives you the verb.


References & Further Reading

  1. cppreference — std::unique_ptr
  2. cppreference — std::shared_ptr
  3. cppreference — RAII (Resource Acquisition Is Initialization)
  4. cppreference — Lambda Expressions
  5. cppreference — Move Semantics
  6. C++ Core Guidelines — Bjarne Stroustrup & Herb Sutter
  7. JSI Header — jsi.h (API Surface, facebook/react-native)

Top comments (0)