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
  • Loading branch information
mrousavy committed Sep 17, 2024
1 parent 97f36fd commit 2b062a5
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 5 deletions.
25 changes: 25 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,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
35 changes: 35 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,35 @@
//
// 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;

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 2b062a5

Please sign in to comment.