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 instantiated from JS using createHybridObject(...):

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
}

Base Methods

Every Hybrid Object has base methods and properties, like name, toString() and equals(..):

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

Additionally, 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.

Inheritance

As the name suggests, Hybrid Objects are object-oriented, meaning they have full support for inheritance and abstraction. A Hybrid Object can either inherit from other Hybrid Objects, or satisfy a common interface.

Inherit from other Hybrid Objects

Each Hybrid Object has a proper JavaScript prototype chain, created automatically and lazily. When a Hybrid Object inherits from another Hybrid Object, it extends the prototype chain:

interface Media extends HybridObject {
readonly width: number
readonly height: number
saveToFile(): Promise<void>
}

type ImageFormat = 'jpg' | 'png'
interface Image extends HybridObject, Media {
readonly format: ImageFormat
}

const image1 = NitroModules.createHybridObject<Image>('Image')
const image2 = NitroModules.createHybridObject<Image>('Image')

Inherit from a common interface

With Nitrogen, you can define a common TypeScript interface that multiple Hybrid Objects inherit from. This non-HybridObject interface (Media) will not be a separate type on the native side, but all Hybrid Objects that extend from it will satisfy the TypeScript type:

interface Media {
readonly width: number
readonly height: number
}

interface Image extends HybridObject, Media {}
interface Video extends HybridObject, Media {}

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.

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);
});
}
}