Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-native-worklet-turbomodule

Calling a C++ TurboModule from a react-native-worklets Worklet Runtime — so the native code runs off the JS thread.

A minimal, fully working proof-of-concept built on the worklets library's new Bundle Mode. One TurboModule, one method, and an example app that calls it from five different places and prints exactly which JS runtime and which OS thread executed the native code.

Example app screenshot — the same TurboModule method reporting different runtimes and threads

TL;DR — the result

The module's only method returns the address of the jsi::Runtime& it was called with, plus the OS thread it ran on:

call site runtime thread off the JS thread?
called directly 0x10c4b1740 (RN) 29848971 …runtime.JavaScript
runOnUISync 0x11b0ec738 (UI) 29848971 …runtime.JavaScript
runOnRuntimeSync 0x11c7323b8 (worker) 29848971 …runtime.JavaScript
runOnUIAsync 0x11b0ec738 (UI) 29848845 main thread
runOnRuntimeAsync 0x11c7323b8 (worker) 29849264 demo-worker_queue

Two takeaways:

  • It works — the C++ TurboModule method genuinely executes on the worklet runtime's own thread (demo-worker_queue), with the worklet runtime's own jsi::Runtime.
  • sync ≠ off-thread — the *Sync schedulers enter the worklet runtime on the calling thread. The runtime pointer changes, the thread does not. Only the *Async schedulers actually move the work.

The value= counter is a single std::atomic<int> in C++ and increments 1…5 across all five calls — proving every runtime talks to the same native module instance, not a copy.

Why this is interesting

TurboModules normally live on the JS thread. Worklet runtimes (the UI runtime, or workers made with createWorkletRuntime) can't reach them — in Bundle Mode the registry is explicitly shimmed to refuse (the app demonstrates this live, last row of the screenshot):

[Worklets] Accessing TurboModules is not allowed on Worklet Runtimes. Requested: "WorkletTurbomodule".

This repo shows the combination that gets around it with zero patches to worklets itself:

  1. A C++ TurboModule (NativeWorkletTurbomoduleCxxSpec). A C++ TurboModule method runs synchronously on the caller's thread and receives the caller's jsi::Runtime&. An ObjC/Java module would hop to its own module queue — the work would leave the worklet thread again.

  2. Closure capture instead of registry lookup. The whole library-side trick is src/getRuntimeInfo.native.tsx:

    import WorkletTurbomodule from './NativeWorkletTurbomodule'; // registry, RN Runtime, once
    
    export function getRuntimeInfo(): string {
      'worklet';
      return WorkletTurbomodule.getRuntimeInfo(); // captured in the closure
    }

    TurboModuleRegistry.getEnforcing runs once, on the RN Runtime. The worklet captures the resulting object. When the worklet is serialized to another runtime, worklets recognises the TurboModule shape (prototype is a JSI host object) and routes it through its C++ createSerializableTurboModuleLike — the host object is shared with the target runtime, not copied. TurboModule::get then hands out host functions bound to whichever runtime asks.

  3. Bundle Mode, so worklet runtimes have the full module system (__r) and worklets are real Metro modules unpacked as __r(workletHash).default(closure) — no eval, real imports inside worklets.

sequenceDiagram
    participant RN as RN Runtime (JS thread)
    participant W as Worklets serializer
    participant WR as Worker Runtime (own thread)
    participant TM as C++ TurboModule

    RN->>RN: TurboModuleRegistry.getEnforcing('WorkletTurbomodule')
    RN->>W: runOnRuntimeAsync(getRuntimeInfo)
    Note over W: worklet → {hash, closure}<br/>closure has the TurboModule →<br/>createSerializableTurboModuleLike<br/>(host object shared, not copied)
    W->>WR: schedule on worker thread
    WR->>WR: __r(hash).default(closure)
    WR->>TM: getRuntimeInfo()  — synchronous JSI call
    TM-->>WR: "runtime=0x11c73… thread=demo-worker_queue"
Loading

What's in the repo

├── src/
│   ├── NativeWorkletTurbomodule.ts     # codegen spec (1 method)
│   └── getRuntimeInfo.native.tsx       # the worklet-compatible wrapper (the trick)
├── cpp/
│   └── WorkletTurbomoduleImpl.cpp      # C++ TurboModule: reports runtime + thread
├── patches/
│   ├── metro.patch                     # Bundle Mode: index .worklets/* virtual modules
│   └── metro-runtime.patch             # Bundle Mode: Fast Refresh → worklet runtimes
└── example/                            # the app from the screenshot
    ├── babel.config.js                 # worklets plugin, bundleMode: true
    ├── metro.config.js                 # getBundleModeMetroConfig(...)
    └── src/App.tsx                     # calls the module from 5 places

The library is a standard create-react-native-library turbo-module (C++ variant); everything Bundle-Mode-specific is listed above.

Bundle Mode setup notes

As per the setup guide, plus two things worth calling out:

  • getBundleModeMetroConfig(config) over bundleModeMetroConfig. The function form composes with an existing resolveRequest (here: the monorepo resolver that points the example app at the library source). The plain config object would replace it.
  • The metro patches are applied via yarn's patch: protocol in the root package.json resolutions — no patch-package, reproducible from a plain yarn install. They're the upstream worklets patches regenerated against metro 0.83.7.

You can verify Bundle Mode is really on in a served bundle:

curl -s "http://localhost:8081/index.bundle?platform=ios&dev=true" | grep -c "\.worklets/"

Running it

Verified with React Native 0.83.10, react-native-worklets 0.11.3, Xcode 26, iOS 26 simulator. The native part is plain C++/pthreads, so the same code path should apply on Android (not exercised yet).

yarn
yarn prepare        # generates ios/generated (codegen) — must run BEFORE pod install
cd example/ios && pod install && cd -
yarn example start --reset-cache
# in another terminal:
yarn example ios

Caveats

  • Thread safety is on you. The same C++ module instance is entered from several threads. This demo uses std::atomic; a real module needs deliberate synchronization.
  • Supported machinery, undocumented use case. Everything used here is existing worklets API surface ('worklet', closure capture, the TurboModule branch of the serializer) — but no worklets doc promises TurboModule calls on worklet runtimes. Tested against worklets 0.11.3.
  • Only C++ TurboModules stay on the worklet thread. See "Why this is interesting" above.

License

MIT

About

C++ TurboModule executed on a react-native-worklets Worklet Runtime (off the JS thread) via Bundle Mode — working PoC with example app

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages