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..1ac41b3db --- /dev/null +++ b/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.cpp @@ -0,0 +1,25 @@ +// +// BoxedHybridObject.cpp +// NitroModules +// +// Created by Marc Rousavy on 17.09.24. +// + +#include "BoxedHybridObject.hpp" + +namespace margelo::nitro { + +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..15edaa128 --- /dev/null +++ b/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp @@ -0,0 +1,35 @@ +// +// 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; + +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