Skip to content

Latest commit

 

History

History

react-native-nitro-modules

Nitrogen

react-native-nitro-modules is a core library that contains highly efficient statically compiled JS to C++ bindings.

It uses JSI to generate C++ templates that can bridge virtually any JS type to a C++ type with minimal overhead.

Installation

Inside an app

Install react-native-nitro-modules as a dependency in your react-native app:

npm i react-native-nitro-modules
cd ios && pod install

Inside a nitro module library

If you are building a nitro module yourself, add react-native-nitro-modules as a peerDependency into your library's package.json:

{
  ...
  "peerDependencies": {
    ...
    "react-native-nitro-modules": "*"
  },
}

Then install react-native-nitro-modules as a normal dependency in your library's example/ app as seen above.

Usage

react-native-nitro-modules can either be used with-, or without nitrogen, or mixed (some objects are automatically generated, some manually).

With Nitrogen

When using Nitrogen, all the bindings are automatically generated. You only need to implement C++, Swift or Kotlin interfaces inside your codebase.

Without Nitrogen

All C++ bindings are bridged to JS using "Hybrid Objects".

A Hybrid Object can have both methods and properties (get and set). Create a C++ Hybrid Object by inheriting from HybridObject:

#include <NitroModules/HybridObject.hpp>

using namespace margelo::nitro;

class MyHybridObject: public HybridObject {
public:
  explicit MyHybridObject(): HybridObject(TAG) {}

public:
  // Property (get)
  double getNumber() { return 13; }
  // Property (set)
  void setNumber(double value) { }
  // Method
  double add(double left, double right) { return left + right; }

public:
  void loadHybridMethods() override {
    // Call base method to make sure we properly inherit `toString()` and `equals()`
    HybridObject::loadHybridMethods();
    // Register all methods that need to be exposed to JS
    registerHybrids(this, [](Prototype& prototype) {
      prototype.registerHybridGetter("number", &MyHybridObject::getNumber);
      prototype.registerHybridSetter("number", &MyHybridObject::setNumber);
      prototype.registerHybridMethod("add", &MyHybridObject::add);
    });
  }

private:
  static constexpr auto TAG = "MyHybrid";
};

The MyHybridObject can then be registered in the HybridObjectRegistry at app startup:

#include <NitroModules/HybridObjectRegistry.hpp>

// Call this at app startup to register the HybridObjects
void load() {
  HybridObjectRegistry::registerHybridObjectConstructor(
    "MyHybrid",
    []() -> std::shared_ptr<HybridObject> {
      return std::make_shared<MyHybridObject>();
    }
  );
}

Inside your MyHybridObject, you can use standard C++ types which will automatically be converted to JS using Nitro's JSIConverter<T> interface.

The following C++ / JS types are supported out of the box:

JS Type C++ Type Swift Type Kotlin Type
number double / int / float Double Double
boolean bool Bool Boolean
string std::string String String
bigint int64_t / uint64_t Int64 Long
T[] std::vector<T> [T] Array<T> / PrimitiveArray
[A, B, C, ...] std::tuple<A, B, C, ...> (A, B, C) 🟡  (#38)
A | B | C | ... std::variant<A, B, C, ...> Variant_A_B_C Variant_A_B_C
Record<string, T> std::unordered_map<std::string, T> Dictionary<String, T> Map<std::string, T>
T? std::optional<T> T? T?
(T...) => void std::function<void (T...)> @escaping (T...) -> Void (T...) -> Unit
(T...) => R std::function<std::future<R> (T...)>
Promise<T> std::shared_ptr<Promise<T>> Promise<T> Promise<T>
{ ... } std::shared_ptr<AnyMap> AnyMapHolder AnyMap
ArrayBuffer std::shared_ptr<ArrayBuffer> ArrayBufferHolder ArrayBuffer
..any HybridObject std::shared_ptr<HybridObject> HybridObjectSpec HybridObject
..any interface T T T
..any enum T T T
..any union T T T

Since the JSIConverter<T> is just a template, you can extend it with any other custom types by overloading the interface.

For example, to add support for an enum, overload JSIConverter<MyEnum>:

#include <NitroModules/JSIConverter.hpp>

enum class MyEnum {
  FIRST = 0,
  SECOND = 1
};

namespace margelo::nitro {
  template <>
  struct JSIConverter<MyEnum> {
    static inline MyEnum fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
      int intValue = JSIConverter<int>::fromJSI(runtime, arg);
      return static_cast<MyEnum>(intValue);
    }
    static inline jsi::Value toJSI(jsi::Runtime& runtime, MyEnum arg) {
      int intValue = static_cast<int>(arg);
      return JSIConverter<int>::toJSI(runtime, intValue);
    }
  };
}

Once the JSIConverter<T> for MyEnum is defined, you can use the type MyEnum in C++ methods, getters and setters of HybridObjects.

And on the JS side, you can simply treat the returned number (int) as a MyEnum:

enum MyEnum {
  FIRST = 0,
  SECOND = 1
}
const value = myHybridObject.getEnumValue() // <-- typed as `MyEnum` instead of `number`

Make sure to always include the header that defines the JSIConverter<MyEnum> overload inside the MyHybridObject file, as this is where the JSIConverter<T> overloads are accessed from.

Nitrogen can automatically generate such JSIConverter<T> extensions for enums, TypeScript unions, and even structs/objects - so it is generally recommended to use nitrogen.