Skip to main content

Hybrid Objects

A Hybrid Object is a native object that can also be used from JS. Similar to a JavaScript class, Hybrid Objects can have instance methods, properties (get + set), static methods, constructors (new) and inheritance chains via prototypes.

Math.nitro.ts
@iOS(...)
@Android(...)
export declare class Math extends HybridObject {
static add(a: number, b: number): number
}
export * from '#nitro/Math'
HybridMath.swift
class HybridMath : HybridMathSpec {
static func add(a: Double, b: Double) -> Double {
return a + b
}
}

Working with Hybrid Objects​

If a Hybrid Object has static methods, those can be called directly on the class:

import { Math } from './Math.nitro'

const result = Math.add(5, 7)

Similarly, if a Hybrid Object has a constructor(...), instances of the Hybrid Object can be created via new:

import { Math } from './Math.nitro'

const math = new Math(...)

A Hybrid Object can also create other Hybrid Objects:

Image.nitro.ts
@iOS(...)
@Android(...)
export declare class Image extends HybridObject {
readonly width: number
readonly height: number
saveToFile(path: string): Promise<void>
}
export * from '#nitro/Image'

@iOS(...)
@Android(...)
export declare class ImageLoader extends HybridObject {
static loadImageFromWeb(path: string): Promise<Image>
static loadImageFromFile(path: string): Image
static loadImageFromResources(name: string): Image
}
export * from '#nitro/ImageLoader'

Base Methods​

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

const math = new Math()
const anotherMath = math

console.log(math.toString()) // "[HybridObject Math]"
console.log(math.equals(anotherMath)) // true

Overriding base methods​

In your implementation, you can override HybridObject's base methods like toString() or dispose():

HybridMath.swift
class HybridMath : HybridMathSpec {
func toString() -> String {
return "{HybridMath}"
}
}

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. Keep shared instances alive while other code still uses them. Static methods belong to the class and do not require an instance to dispose.

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
@iOS({
language: 'swift',
implementation: { className: 'HybridMath' }
})
@Android({
language: 'kotlin',
implementation: { className: 'HybridMath' }
})
export declare class Math extends HybridObject {
static add(a: number, b: number): number
}

Running nitrogen generates HybridMathSpec, a protocol/base-class composition in Swift and an abstract class in Kotlin. Extend it in your implementation:

HybridMath.swift
class HybridMath : HybridMathSpec {
static 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 fully support 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. Set allowInheritance: true and allowMultipleImplementations: true on the base spec's platform decorators:

@iOS({
// ...
allowInheritance: true,
allowMultipleImplementations: true
})
export declare class Media extends HybridObject {
readonly width: number
readonly height: number
saveToFile(): Promise<void>
}
export * from '#nitro/Media'

type ImageFormat = 'jpg' | 'png'
@iOS(...)
@Android(...)
export declare class Image extends Media {
constructor(bytes: ArrayBuffer)
readonly format: ImageFormat
}
export * from '#nitro/Image'

const image1 = new Image(FIRST_IMAGE_BYTES)
const image2 = new Image(SECOND_IMAGE_BYTES)
image2.saveToFile()

Inherit from a common interface​

Hybrid Objects can implement a common TypeScript interface. Declare the members on each class too, so Nitrogen generates their native bindings. The implements clause checks the TypeScript shape without adding a native base class:

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

@iOS(...)
@Android(...)
export declare class Image extends HybridObject implements Media {
readonly width: number
readonly height: number
}
export * from '#nitro/Image'

@iOS(...)
@Android(...)
export declare class Video extends HybridObject implements Media {
readonly width: number
readonly height: number
}
export * from '#nitro/Video'

Memory Size (memorySize)​

Since a HybridObject'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 by overriding the memorySize/getExternalMemorySize() accessors, which can account for any external heap allocations you perform:

class HybridImage : HybridImageSpec {
private let cgImage: CGImage

init(cgImage: CGImage) {
self.cgImage = cgImage
super.init()
}

var memorySize: Int {
return cgImage.bytesPerRow * cgImage.height
}
}

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: public HybridObject {
public:
struct Class {
using Base = HybridObject::Class;
static constexpr const char* name = "Math";
static const std::shared_ptr<const HybridClass>& get() {
static const auto hybridClass = HybridClass::create<Class>([](HybridClass& clazz) {
clazz.registerRawHybridMethod("sayHello", 2, &HybridMath::sayHello);
});
return hybridClass;
}
};

const std::shared_ptr<const HybridClass>& getHybridClass() override {
return Class::get();
}

jsi::Value sayHello(jsi::Runtime& runtime,
const jsi::Value& thisValue,
const jsi::Value* args,
size_t count);
};