Hybrid Objects
A Hybrid Object is a native object that can be used from JS like any other object. They can have natively implemented methods, as well as properties (get + set).
interface Math extends HybridObject {
readonly pi: number
add(a: number, b: number): number
}
class HybridMath : HybridMathSpec {
var pi: Double {
return Double.pi
}
func add(a: Double, b: Double) -> Double {
return a + b
}
}
Working with Hybrid Objects
Hybrid Objects can be created using createHybridObject(...)
if they have been registered on the native side:
const math = NitroModules.createHybridObject<Math>("Math")
const result = math.add(5, 7)
A Hybrid Object can also create other Hybrid Objects:
interface Image extends HybridObject {
readonly width: number
readonly height: number
saveToFile(path: string): Promise<void>
}
interface ImageFactory extends HybridObject {
loadImageFromWeb(path: string): Promise<Image>
loadImageFromFile(path: string): Image
loadImageFromResources(name: string): Image
}
Each instance of a Hybrid Object reflects it's actual native memory size, so the JavaScript runtime can garbage-collect unused objects more efficiently. Additionally, Hybrid Objects have proper JavaScript prototypes, which are shared between all instances of the same type:
Base Methods
Every Hybrid Object has base methods and properties:
const math = NitroModules.createHybridObject<Math>("Math")
const anotherMath = math
console.log(math.name) // "Math"
console.log(math.toString()) // "[HybridObject Math]"
console.log(math.equals(anotherMath)) // true
dispose()
Every Hybrid Object has a dispose()
method.
Usually, you should not need to manually dispose Hybrid Objects as the JS garbage collector will delete any unused objects anyways.
Also, most Hybrid Objects in Nitro are just statically exported singletons, in which case they should never be deleted throughout the app's lifetime.
In some rare, often performance-critical- cases it is beneficial to eagerly destroy any Hybrid Objects, which is why dispose()
exists.
For example, VisionCamera uses dispose()
to clean up already processed Frames to make room for new incoming Frames:
const onFrameListener = (frame: Frame) => {
doSomeProcessing(frame)
frame.dispose()
}
Implementation
Hybrid Objects can be implemented in C++, Swift or Kotlin:
- With Nitrogen ✨
- Manually
Nitrogen will ✨ automagically ✨ generate native specifications for each Hybrid Object based on a given TypeScript definition:
interface Math extends HybridObject<{ ios: 'swift', android: 'kotlin' }> {
readonly pi: number
add(a: number, b: number): number
}
Running nitrogen will generate the native Swift and Kotlin protocol "HybridMathSpec
", that now just needs to be implemented in a class:
- Swift
- Kotlin
class HybridMath : HybridMathSpec {
public var hybridContext = margelo.nitro.HybridContext()
public var memorySize: Int {
return getSizeOf(self)
}
public var pi: Double {
return Double.pi
}
public func add(a: Double, b: Double) throws -> Double {
return a + b
}
}
class HybridMath : HybridMathSpec() {
override val memorySize: Long
get() = 0L
override var pi: Double
get() = Double.PI
override fun add(a: Double, b: Double): Double {
return a + b
}
}
For more information, see the Nitrogen documentation.
To implement a Hybrid Object without nitrogen, you just need to create a C++ class that inherits from the HybridObject
base class, and override loadHybridMethods()
:
class HybridMath: public HybridObject {
public:
HybridMath(): HybridObject(NAME) { }
public:
double add(double a, double b);
protected:
void loadHybridMethods() override;
private:
static constexpr auto NAME = "Math";
};
double HybridMath::add(double a, double b) {
return a + b;
}
void HybridMath::loadHybridMethods() {
// register base methods (toString, ...)
HybridObject::loadHybridMethods();
// register custom methods (add)
registerHybrids(this, [](Prototype& proto) {
proto.registerHybridMethod(
"add",
&HybridMath::add
);
});
}
A Hybrid Object should also override getExternalMemorySize()
to properly reflect native memory size:
class HybridMath: public HybridObject {
public:
// ...
size_t getExternalMemorySize() override {
return sizeOfSomeImageWeAllocated;
}
}
Optionally, you can also override toString()
and dispose()
for custom behaviour.
Memory Size (memorySize
)
Since it's implementation is in native code, the JavaScript runtime does not know the actual memory size of a Hybrid Object.
Nitro allows Hybrid Objects to declare their memory size via the memorySize
/getExternalMemorySize()
accessors, which can account for any external heap allocations you perform:
class HybridImage : HybridImageSpec {
private var cgImage: CGImage
public var memorySize: Int {
let imageSize = cgImage.width * cgImage.height * cgImage.bytesPerPixel
return getSizeOf(self) + imageSize
}
}
Any unused Image
objects can now be deleted sooner by the JS garbage collector, preventing memory pressures or frequent garbage collector calls.
It is safe to return 0
here, but recommended to somewhat closely estimate the actual size of native object if possible.
Overriding or adding methods/properties
In a C++ Hybrid Object you can override or add methods and properties by overriding the loadHybridMethods()
method, and calling registerHybrids(...)
:
class HybridMath: HybridMathSpec {
public:
std::string sayHello();
void loadHybridMethods() override {
// register base protoype
HybridMathSpec::loadHybridMethods();
// register all methods we override here
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("sayHello", &HybridMath::sayHello);
});
}
}
If a method or property is added that already exists in a base class it will be overridden - similar to how a JS class can override methods/properties from it's base class. The prototype's prototype will still contain the original method/property.
Raw JSI methods
If for some reason Nitro's typing system is not sufficient in your case, you can also create a raw JSI method using registerRawHybridMethod(...)
to directly work with the jsi::Runtime
and jsi::Value
types:
class HybridMath: HybridMathSpec {
public:
jsi::Value sayHello(jsi::Runtime& runtime,
const jsi::Value& thisValue,
const jsi::Value* args,
size_t count);
void loadHybridMethods() override {
// register base protoype
HybridMathSpec::loadHybridMethods();
// register all methods we override here
registerHybrids(this, [](Prototype& prototype) {
prototype.registerRawHybridMethod("sayHello", 0, &HybridMath::sayHello);
});
}
}