Skip to content

Commit

Permalink
feat: Add NitroModules.box(...) to support using Nitro Modules from…
Browse files Browse the repository at this point in the history
… any Runtime/Worklets context (#138)

* feat: Add `NitroModules.box(...)` to support using Nitro Modules from any Runtime/Worklets context

* feat: Add `getPropertyNames` for `BoxedHybridObject`

* docs: Create Worklets/Threading docs
  • Loading branch information
mrousavy authored Sep 17, 2024
1 parent 97f36fd commit 1128d6a
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 7 deletions.
53 changes: 53 additions & 0 deletions docs/docs/worklets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Worklets/Threading

Nitro itself is fully runtime-agnostic, which means every [Hybrid Object](hybrid-object) can be used from any JS Runtime or Worklet Context.

This allows the caller to call into native Nitro Modules from libraries like [react-native-worklets-core](https://github.com/margelo/react-native-worklets-core), or [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated).
You can use a Nitro [Hybrid Object](hybrid-object) on the default React JS context, on the UI context, or on any other background worklet context.

<Tabs groupId="worklet-library">
<TabItem value="rnwc" label="Worklets Core" default>
```ts
const math = NitroModules.createHybridObject<Math>('Math')
const boxed = NitroModules.box(math)

const context = Worklets.createContext('DummyContext')
context.runAsync(() => {
'worklet'
const unboxed = boxed.unbox()
console.log(unboxed.add(5, 3)) // --> 8
})
```
</TabItem>
<TabItem value="rea" label="Reanimated">
```ts
const math = NitroModules.createHybridObject<Math>('Math')
const boxed = NitroModules.box(math)

runOnUI(() => {
'worklet'
const unboxed = boxed.unbox()
console.log(unboxed.add(5, 3)) // --> 8
})()
```
</TabItem>
</Tabs>

## Boxing

Since Nitro uses newer JSI APIs like `jsi::NativeState` - which current worklet libraries (like [react-native-worklets-core](https://github.com/margelo/react-native-worklets-core) or [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated)) do not yet fully support - Hybrid Objects cannot yet be _directly_ used in worklet contexts - they have to be _boxed_.

A _boxed_ Hybrid Object is a native `jsi::HostObject`, which is supported by worklet libraries. The process is as following:

1. In the runtime your `HybridObject` was created in (probably the default runtime), call `NitroModules.box(...)` to box it.
2. The boxed result can be shared in any (worklet-)runtime if needed.
3. To use the original `HybridObject`, simply call `.unbox()` on it in the desired (worklet-)runtime.
4. The result of `.unbox()` is the original `HybridObject` - you can now call any methods on it as usual.

In future versions of [react-native-worklets-core](https://github.com/margelo/react-native-worklets-core) or [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) we expect fullly automatic `jsi::NativeState` support, which will make boxing obsolete.
1 change: 1 addition & 0 deletions docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const sidebars: SidebarsConfig = {
'errors',
'performance-tips',
'view-components',
'worklets',
'comparison',
'for-users',
],
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1825,11 +1825,11 @@ SPEC CHECKSUMS:
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
FBLazyVector: 38bb611218305c3bc61803e287b8a81c6f63b619
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
hermes-engine: 3b6e0717ca847e2fc90a201e59db36caf04dee88
NitroImage: 0cffeee137c14b8c8df97649626646528cb89b28
NitroModules: a332e719544e7fd7f86eb4175888ae683aca03ee
RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47
RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
RCTDeprecation: 34cbf122b623037ea9facad2e92e53434c5c7422
RCTRequired: 24c446d7bcd0f517d516b6265d8df04dc3eb1219
RCTTypeSafety: ef5e91bd791abd3a99b2c75fd565791102a66352
Expand Down
29 changes: 29 additions & 0 deletions packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// BoxedHybridObject.cpp
// NitroModules
//
// Created by Marc Rousavy on 17.09.24.
//

#include "BoxedHybridObject.hpp"

namespace margelo::nitro {

std::vector<jsi::PropNameID> BoxedHybridObject::getPropertyNames(facebook::jsi::Runtime& runtime) {
return jsi::PropNameID::names(runtime, "unbox");
}

jsi::Value BoxedHybridObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
std::string name = propName.utf8(runtime);

if (name == "unbox") {
return jsi::Function::createFromHostFunction(
runtime, jsi::PropNameID::forUtf8(runtime, "unbox"), 0,
[hybridObject = _hybridObject](jsi::Runtime& runtime, const jsi::Value& thisArg, const jsi::Value* args,
size_t count) -> jsi::Value { return hybridObject->toObject(runtime); });
}

return jsi::Value::undefined();
}

} // namespace margelo::nitro
36 changes: 36 additions & 0 deletions packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Created by Marc Rousavy on 21.02.24.
//

#pragma once

#include "HybridObject.hpp"
#include <jsi/jsi.h>
#include <memory>

namespace margelo::nitro {

using namespace facebook;

/**
* Represents a `HybridObject` that has been boxed into a `jsi::HostObject`.
*
* While `HybridObject`s are runtime agnostic, some threading/worklet libraries do not support copying over objects
* with `jsi::NativeState` and a prototype chain (which is what a `HybridObject` is), so Nitro offers support for
* boxing those `HybridObject`s into a type that those libraries support - which is a `jsi::HostObject`.
*
* Simply call `unbox()` on this `jsi::HostObject` from the new Runtime/context to get the `HybridObject` again.
*/
class BoxedHybridObject : public jsi::HostObject {
public:
explicit BoxedHybridObject(const std::shared_ptr<HybridObject>& hybridObject) : _hybridObject(hybridObject) {}

public:
jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& propName) override;
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& runtime) override;

private:
std::shared_ptr<HybridObject> _hybridObject;
};

} // namespace margelo::nitro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

#include "NativeNitroModules.hpp"
#include "BoxedHybridObject.hpp"
#include "CallInvokerDispatcher.hpp"
#include "Dispatcher.hpp"
#include "HybridObjectRegistry.hpp"
Expand Down Expand Up @@ -87,6 +88,29 @@ jsi::Value NativeNitroModules::get(jsi::Runtime& runtime, const jsi::PropNameID&
return jsi::Value::undefined();
});
}
if (name == "box") {
return jsi::Function::createFromHostFunction(
runtime, jsi::PropNameID::forUtf8(runtime, "box"), 1,
[](jsi::Runtime& runtime, const jsi::Value& thisArg, const jsi::Value* args, size_t count) -> jsi::Value {
jsi::Object object = args[0].asObject(runtime);
#ifdef NITRO_DEBUG
if (!object.hasNativeState(runtime)) {
std::string stringified = args[0].toString(runtime).utf8(runtime);
throw std::runtime_error("Cannot box object " + stringified + " - it does not have a NativeState!");
}
#endif

std::shared_ptr<jsi::NativeState> nativeState = object.getNativeState(runtime);
std::shared_ptr<HybridObject> maybeHybridObject = std::dynamic_pointer_cast<HybridObject>(nativeState);
if (maybeHybridObject == nullptr) {
std::string stringified = args[0].toString(runtime).utf8(runtime);
throw std::runtime_error("Cannot box object " + stringified + " - it has a NativeState, but it's not a HybridObject!");
}

auto boxed = std::make_shared<BoxedHybridObject>(maybeHybridObject);
return jsi::Object::createFromHostObject(runtime, boxed);
});
}
if (name == "buildType") {
#ifdef NITRO_DEBUG
return jsi::String::createFromAscii(runtime, "debug");
Expand Down
42 changes: 37 additions & 5 deletions packages/react-native-nitro-modules/src/NitroModules.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { getNativeNitroModules } from './NitroModulesTurboModule'
import type { HybridObject } from './HybridObject'

// TODO: Do we wanna support such constructors?
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExtractConstructors<T> = {
[K in keyof T as K extends `constructor` ? `create` : never]: T[K]
/**
* Represents a boxed {@linkcode HybridObject} that can later be unboxed again.
* This is implemented as a `jsi::HostObject`.
*/
export interface BoxedHybridObject<T extends HybridObject> {
/**
* Unboxes the {@linkcode HybridObject}.
* This can be called from a different Runtime than the one it was boxed in.
*/
unbox(): T
}

/**
Expand Down Expand Up @@ -73,4 +78,31 @@ export const NitroModules = {
const nitro = getNativeNitroModules()
return nitro.buildType
},
/**
* Boxes the given {@linkcode hybridObject} into a {@linkcode BoxedHybridObject<T>}, which can
* later be unboxed in a separate Runtime.
*
* While Nitro is runtime-agnostic and all `HybridObject`s can be used from a any Runtime,
* some threading/worklet libraries (like [react-native-worklets-core](https://github.com/margelo/react-native-worklets-core))
* do not yet support copying over `HybridObject`s as they use newer JSI APIs like `jsi::NativeState`.
*
* While those APIs are not yet available, you can still use every Nitro Hybrid Object in a separate
* Runtime/Worklet context by just boxing it yourself:
*
* @example
* ```ts
* const something = NitroModules.createHybridObject<Something>('Something')
* const boxed = NitroModules.box(something)
* const context = Worklets.createContext('DummyContext')
* context.runAsync(() => {
* 'worklet'
* const unboxed = boxed.unbox()
* console.log(unboxed.name) // --> "Something"
* })
* ```
*/
box<T extends HybridObject>(hybridObject: T): BoxedHybridObject<T> {
const nitro = getNativeNitroModules()
return nitro.box(hybridObject) as BoxedHybridObject<T>
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface NativeNitroSpec extends TurboModule {
hasNativeState(obj: UnsafeObject): boolean
removeNativeState(obj: UnsafeObject): void
buildType: 'debug' | 'release'
box(obj: UnsafeObject): UnsafeObject
}

let turboModule: NativeNitroSpec | undefined
Expand Down

0 comments on commit 1128d6a

Please sign in to comment.