Skip to main content

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).

Math.nitro.ts
interface Math extends HybridObject {
readonly pi: number
add(a: number, b: number): number
}
HybridMath.swift
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:

Image.nitro.ts
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:

Nitrogen will ✨ automagically ✨ generate native specifications for each Hybrid Object based on a given TypeScript definition:

Math.nitro.ts
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:

HybridMath.swift
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
}
}

For more information, see the Nitrogen documentation.

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.

tip

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(...):

HybridMath.hpp
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:

HybridMath.hpp
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);
});
}
}