From 1128d6a3c6458b1af93d846d8050dcf31b2d9b71 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 17 Sep 2024 14:45:28 +0200 Subject: [PATCH] feat: Add `NitroModules.box(...)` to support using Nitro Modules from 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 --- docs/docs/worklets.md | 53 +++++++++++++++++++ docs/sidebars.ts | 1 + example/ios/Podfile.lock | 4 +- .../cpp/core/BoxedHybridObject.cpp | 29 ++++++++++ .../cpp/core/BoxedHybridObject.hpp | 36 +++++++++++++ .../cpp/turbomodule/NativeNitroModules.cpp | 24 +++++++++ .../src/NitroModules.ts | 42 +++++++++++++-- .../src/NitroModulesTurboModule.ts | 1 + 8 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 docs/docs/worklets.md create mode 100644 packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.cpp create mode 100644 packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp diff --git a/docs/docs/worklets.md b/docs/docs/worklets.md new file mode 100644 index 000000000..aa925bf69 --- /dev/null +++ b/docs/docs/worklets.md @@ -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. + + + + ```ts + const math = NitroModules.createHybridObject('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 + }) + ``` + + + ```ts + const math = NitroModules.createHybridObject('Math') + const boxed = NitroModules.box(math) + + runOnUI(() => { + 'worklet' + const unboxed = boxed.unbox() + console.log(unboxed.add(5, 3)) // --> 8 + })() + ``` + + + +## 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. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 4b91a240d..b210941c9 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -41,6 +41,7 @@ const sidebars: SidebarsConfig = { 'errors', 'performance-tips', 'view-components', + 'worklets', 'comparison', 'for-users', ], diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index aa1345e1f..136f66f18 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -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 diff --git a/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.cpp b/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.cpp new file mode 100644 index 000000000..98dc7d9f8 --- /dev/null +++ b/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.cpp @@ -0,0 +1,29 @@ +// +// BoxedHybridObject.cpp +// NitroModules +// +// Created by Marc Rousavy on 17.09.24. +// + +#include "BoxedHybridObject.hpp" + +namespace margelo::nitro { + +std::vector 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 diff --git a/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp b/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp new file mode 100644 index 000000000..7e0bd6ac5 --- /dev/null +++ b/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp @@ -0,0 +1,36 @@ +// +// Created by Marc Rousavy on 21.02.24. +// + +#pragma once + +#include "HybridObject.hpp" +#include +#include + +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) {} + +public: + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& propName) override; + std::vector getPropertyNames(jsi::Runtime& runtime) override; + +private: + std::shared_ptr _hybridObject; +}; + +} // namespace margelo::nitro diff --git a/packages/react-native-nitro-modules/cpp/turbomodule/NativeNitroModules.cpp b/packages/react-native-nitro-modules/cpp/turbomodule/NativeNitroModules.cpp index 8d17e465e..19427955b 100644 --- a/packages/react-native-nitro-modules/cpp/turbomodule/NativeNitroModules.cpp +++ b/packages/react-native-nitro-modules/cpp/turbomodule/NativeNitroModules.cpp @@ -6,6 +6,7 @@ // #include "NativeNitroModules.hpp" +#include "BoxedHybridObject.hpp" #include "CallInvokerDispatcher.hpp" #include "Dispatcher.hpp" #include "HybridObjectRegistry.hpp" @@ -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 nativeState = object.getNativeState(runtime); + std::shared_ptr maybeHybridObject = std::dynamic_pointer_cast(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(maybeHybridObject); + return jsi::Object::createFromHostObject(runtime, boxed); + }); + } if (name == "buildType") { #ifdef NITRO_DEBUG return jsi::String::createFromAscii(runtime, "debug"); diff --git a/packages/react-native-nitro-modules/src/NitroModules.ts b/packages/react-native-nitro-modules/src/NitroModules.ts index ebae9264f..08574188b 100644 --- a/packages/react-native-nitro-modules/src/NitroModules.ts +++ b/packages/react-native-nitro-modules/src/NitroModules.ts @@ -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 = { - [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 { + /** + * Unboxes the {@linkcode HybridObject}. + * This can be called from a different Runtime than the one it was boxed in. + */ + unbox(): T } /** @@ -73,4 +78,31 @@ export const NitroModules = { const nitro = getNativeNitroModules() return nitro.buildType }, + /** + * Boxes the given {@linkcode hybridObject} into a {@linkcode BoxedHybridObject}, 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') + * const boxed = NitroModules.box(something) + * const context = Worklets.createContext('DummyContext') + * context.runAsync(() => { + * 'worklet' + * const unboxed = boxed.unbox() + * console.log(unboxed.name) // --> "Something" + * }) + * ``` + */ + box(hybridObject: T): BoxedHybridObject { + const nitro = getNativeNitroModules() + return nitro.box(hybridObject) as BoxedHybridObject + }, } diff --git a/packages/react-native-nitro-modules/src/NitroModulesTurboModule.ts b/packages/react-native-nitro-modules/src/NitroModulesTurboModule.ts index b8a0e4f69..20f318548 100644 --- a/packages/react-native-nitro-modules/src/NitroModulesTurboModule.ts +++ b/packages/react-native-nitro-modules/src/NitroModulesTurboModule.ts @@ -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