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.
@iOS(...)
@Android(...)
export declare class Math extends HybridObject {
static add(a: number, b: number): number
}
export * from '#nitro/Math'
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:
@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():
- Swift
- Kotlin
- C++
class HybridMath : HybridMathSpec {
func toString() -> String {
return "{HybridMath}"
}
}
class HybridMath : HybridMathSpec() {
override fun toString(): String {
return "{HybridMath}"
}
}
class HybridMath: public HybridMathSpec {
std::string toString() override {
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:
- With Nitrogen ✨
- Manually
Nitrogen will ✨ automagically ✨ generate native specifications for each Hybrid Object based on a given TypeScript definition:
@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:
- Swift
- Kotlin
class HybridMath : HybridMathSpec {
static func add(a: Double, b: Double) throws -> Double {
return a + b
}
}
class HybridMath : HybridMathSpec() {
companion object {
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 implements its Class:
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();
};
const std::shared_ptr<const HybridClass>& getHybridClass() override {
return Class::get();
}
static double add(double a, double b);
};
const std::shared_ptr<const HybridClass>& HybridMath::Class::get() {
static const auto hybridClass = HybridClass::create<Class>([](HybridClass& clazz) {
clazz.registerHybridStaticMethod("add", &HybridMath::add);
});
return hybridClass;
}
double HybridMath::add(double a, double b) {
return a + b;
}
Register the descriptor and expose it to JavaScript as shown in the manual setup guide.
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.
- With Nitrogen ✨
- Manually
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'
Inherit from other Hybrid Objects
To inherit from another Hybrid Object in C++, define the inheritance via your Class's Base:
class HybridImage: public HybridMedia {
public:
struct Class {
using Base = HybridMedia::Class;
static constexpr const char* name = "Image";
static const std::shared_ptr<const HybridClass>& get();
};
const std::shared_ptr<const HybridClass>& getHybridClass() override {
return Class::get();
}
// Image specific methods
ImageFormat getFormat();
};
Then, simply register your own methods - HybridClass already extended HybridMedia's prototype:
const std::shared_ptr<const HybridClass>& HybridImage::Class::get() {
static const auto hybridClass = HybridClass::create<Class>([](HybridClass& clazz) {
clazz.registerHybridMethod("getFormat", &HybridImage::getFormat);
});
return hybridClass;
}
Override base methods/properties
In a C++ Hybrid Object, you can also override base methods and properties by just registering them again on the HybridClass:
const std::shared_ptr<const HybridClass>& HybridImage::Class::get() {
static const auto hybridClass = HybridClass::create<Class>([](HybridClass& clazz) {
clazz.registerHybridMethod("getMediaType", &HybridImage::getFormat);
});
return hybridClass;
}
The HybridImage's prototype's prototype (= HybridMedia's prototype) will still contain the original method/property.
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.
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:
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);
};