In this article I would like to look at the creation of a React-Native C++ Turbo Module for iOS and Android from scratch. As a basis, I took the existing react-native-fs
library and tried to completely rewrite it with a C++ interface, as far as possible.
I'll try to cover basic examples from the documentation, as well as non-obvious things (like working with platform-dependent code, JNI, threads).
Rewriting old libraries to Bridgeless architecture using C++ will most likely increase the speed of work several times (and sometimes tens of times). In my library I managed to increase the speed of methods for Android by ~7 times, for iOS by ~2.3 times (you can see the benchmarks at the link at the bottom of the article).
As references I will use the official documentation, as well as the stable React-Native MMKV library.
I decided not to waste time creating may own component architecture and used the react-native-mmkv
architecture.
As a result, we will have two modules:
-
RNFSTurboPlatformContextModule (TurboModule) - a platform-specific module that returns all the standard paths (like
DocumentDirectory
) - RNFSTurboModule (C++ TurboModule) - the main module with the implementation of all methods.
In this article I will only consider the creation of several exists methods.
RNFSTurboPlatformContextModule (TurboModule)
First of all, we'll write RNFSTurboPlatformContextModule
, because it's the easiest part.
Let's create a TypeScript file src/NativeRNFSTurboPlatformContextModule.ts
, which will contain a description of the specification and initialization of the module itself:
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
getDocumentDirectoryPath(): string;
/* ... */
}
let module: Spec | null;
export function getRNFSTurboPlatformContextTurboModule(): Spec {
try {
if (module == null) {
// 1. Get the TurboModule
module = TurboModuleRegistry.getEnforcing<Spec>(
'RNFSTurboPlatformContextModule',
);
}
return module;
} catch {
// TurboModule could not be found!
throw new Error('Module not found');
}
}
Next, we need to write an implementation of this module. Since this module is platform-dependent, we'll write a separate implementation for Android and iOS.
Let's look at the implementation on iOS. First, we need to create a header file ios/RNFSTurboPlatformContextModule.h
:
#import <Foundation/Foundation.h>
#import <RNFSTurboSpec/RNFSTurboSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNFSTurboPlatformContextModule : NSObject <NativeRNFSTurboPlatformContextModuleSpec>
@end
NS_ASSUME_NONNULL_END
Everything is simple here. Next, we'll write its implementation in ios/RNFSTurboPlatformContextModule.mm
:
#import "RNFSTurboPlatformContextModule.h"
#import <Foundation/Foundation.h>
@implementation RNFSTurboPlatformContextModule
RCT_EXPORT_MODULE()
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams&)params {
return std::make_shared<facebook::react::NativeRNFSTurboPlatformContextModuleSpecJSI>(params);
}
- (NSString*)getDocumentDirectoryPath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
return [paths firstObject];
}
/* ... */
@end
Everything here is according to the official React-Native documentation. The main thing to remember is that the name of the JSI module specifications generated by Codegen will start with Native.
With Android, everything is about the same (we will use com/test/rnfsturbo
package name). First, we create a package file android/src/main/java/com/test/rnfsturbo/RNFSTurboPackage.java
, which loads the module:
package com.test.rnfsturbo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import java.util.HashMap;
import java.util.Map;
public class RNFSTurboPackage extends TurboReactPackage {
@Nullable
@Override
public NativeModule getModule(String name, @NonNull ReactApplicationContext reactContext) {
if (name.equals(RNFSTurboPlatformContextModule.NAME)) {
return new RNFSTurboPlatformContextModule(reactContext);
} else {
return null;
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return () -> {
final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
moduleInfos.put(
RNFSTurboPlatformContextModule.NAME,
new ReactModuleInfo(
RNFSTurboPlatformContextModule.NAME,
RNFSTurboPlatformContextModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
)
);
return moduleInfos;
};
}
}
Then we create the module itself - android/src/main/java/com/test/rnfsturbo/RNFSTurboPlatformContextModule.java
:
package com.test.rnfsturbo;
import android.content.Context;
import com.facebook.react.bridge.ReactApplicationContext;
public class RNFSTurboPlatformContextModule extends NativeRNFSTurboPlatformContextModuleSpec {
private final ReactApplicationContext context;
public static Context externalContext;
public RNFSTurboPlatformContextModule(ReactApplicationContext reactContext) {
super(reactContext);
context = reactContext;
externalContext = reactContext;
}
public static Context getContext() {
return externalContext;
}
@Override
public String getDocumentDirectoryPath() {
return this.context.getFilesDir().getAbsolutePath();
}
/* ... */
}
In this file you can see the creation of a static externalContext
variable for the context. We'll need it to access the context from the JNI (we'll look into this later).
RNFSTurboModule (C++ TurboModule)
Next, we'll implement the main RNFSTurboModule
module. Initially, I wanted to make a full implementation in C++, but later I realized that some methods from react-native-fs
would be almost impossible to reproduce in C++, since there are platform-dependent (such as downloading/uploading files, working with assets, etc). In the native Java/Objective-C code, I left them almost untouched with all copyrights indicated. But I think it will be interesting to figure out how the helper was written for the Java/Objective-C code, especially for Android, since it uses Java Native Interface (JNI) to call Java methods from C++.
Initially, I looked at the implementation of
react-native-mmkv
library and tried to follow its approaches. So I planned for the returned paths fromNativeRNFSTurboPlatformContextModule
to be used inside of the main module. Because of this, I subsequently encountered incompatibility errors between old and new versions of React-Native.
1. First implementation (NativeRNFSTurboModule)
Here I have reviewed the first unsuccessful implementation. If you want to skip it, go directly to Final implementation
So, first we'll create a header file cpp/NativeRNFSTurboModule.h
:
#pragma once
#if __has_include(<React-Codegen/RNFSTurboSpecJSI.h>)
// CocoaPods include (iOS)
#include <React-Codegen/RNFSTurboSpecJSI.h>
#elif __has_include(<RNFSTurboSpecJSI.h>)
// CMake include on Android
#include <RNFSTurboSpecJSI.h>
#else
#error Cannot find react-native-fs-turbo spec! Try cleaning your cache and re-running CodeGen!
#endif
// The RNFSTurboConfiguration type from JS
using RNFSTurboConfig = RNFSTurboModuleConfiguration<
std::string,
std::string,
std::string,
std::string,
std::string,
std::string,
std::string,
std::string,
std::string,
std::string,
std::string
>;
template <> struct Bridging<RNFSTurboConfig> : RNFSTurboModuleConfigurationBridging<RNFSTurboConfig> {};
namespace facebook::react {
// The TurboModule itself
class NativeRNFSTurboModule : public NativeRNFSTurboModuleCxxSpec<NativeRNFSTurboModule> {
public:
NativeRNFSTurboModule(std::shared_ptr<CallInvoker> jsInvoker);
~NativeRNFSTurboModule();
jsi::Object createRNFSTurbo(jsi::Runtime& runtime, RNFSTurboConfig config);
};
} // namespace facebook::react
And its implementation in cpp/NativeRNFSTurboModule.cpp
:
#include "NativeRNFSTurboModule.h"
#include "RNFSTurboHostObject.h"
namespace facebook::react {
NativeRNFSTurboModule::NativeRNFSTurboModule(std::shared_ptr<CallInvoker> jsInvoker)
: NativeRNFSTurboModuleCxxSpec(jsInvoker) {}
NativeRNFSTurboModule::~NativeRNFSTurboModule() {}
jsi::Object NativeRNFSTurboModule::createRNFSTurbo(jsi::Runtime& runtime, RNFSTurboConfig config) {
auto instance = std::make_shared<RNFSTurboHostObject>();
return jsi::Object::createFromHostObject(runtime, instance);
}
} // namespace facebook::react
And this is where the problem was hidden, which later appeared in the new version of React-Native (0.75+). Codegen started generating a ConfigurationBridging
file with a different name. And instead of RNFSTurboModuleConfigurationBridging
, it became called NativeRNFSTurboModuleConfigurationBridging
.
The maintainer of react-native-mmkv
decided to simply drop support of the new architecture for react-native 0.74, which is quite reasonable. But I thought - why do I need RNFSTurboConfig
inside RNFSTurboModule
at all and completely removed it from there (since it didn't give any practical benefit). At the same time, support for React-Native 0.74+ appeared.
2. Final implementation (NativeRNFSTurboModule)
cpp/NativeRNFSTurboModule.h
:
#pragma once
#if __has_include(<React-Codegen/RNFSTurboSpecJSI.h>)
// CocoaPods include (iOS)
#include <React-Codegen/RNFSTurboSpecJSI.h>
#elif __has_include(<RNFSTurboSpecJSI.h>)
// CMake include on Android
#include <RNFSTurboSpecJSI.h>
#else
#error Cannot find react-native-fs-turbo spec! Try cleaning your cache and re-running CodeGen!
#endif
namespace facebook::react {
// The TurboModule itself
class NativeRNFSTurboModule : public NativeRNFSTurboModuleCxxSpec<NativeRNFSTurboModule> {
public:
NativeRNFSTurboModule(std::shared_ptr<CallInvoker> jsInvoker);
~NativeRNFSTurboModule();
jsi::Object createRNFSTurbo(jsi::Runtime& runtime);
};
} // namespace facebook::react
cpp/NativeRNFSTurboModule.cpp:
#include "NativeRNFSTurboModule.h"
#include "RNFSTurboHostObject.h"
namespace facebook::react {
NativeRNFSTurboModule::NativeRNFSTurboModule(std::shared_ptr<CallInvoker> jsInvoker)
: NativeRNFSTurboModuleCxxSpec(jsInvoker) {}
NativeRNFSTurboModule::~NativeRNFSTurboModule() {}
jsi::Object NativeRNFSTurboModule::createRNFSTurbo(jsi::Runtime& runtime) {
auto instance = std::make_shared<RNFSTurboHostObject>();
return jsi::Object::createFromHostObject(runtime, instance);
}
} // namespace facebook::react
Okay, we've got that sorted out. Now let's write a cpp/RNFSTurboHostObject.h
header for the main module:
#pragma once
#include <sys/stat.h>
#include "NativeRNFSTurboModule.h"
using namespace facebook;
class RNFSTurboHostObject : public jsi::HostObject {
public:
RNFSTurboHostObject();
~RNFSTurboHostObject();
public:
jsi::Value get(jsi::Runtime&, const jsi::PropNameID& name) override;
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override;
};
getPropertyNames
will return a list of all available methods. get
calls the required method.
Implementation in cpp/RNFSTurboHostObject.cpp
:
#include "RNFSTurboHostObject.h"
using namespace facebook;
RNFSTurboHostObject::RNFSTurboHostObject() {
// initialization
}
RNFSTurboHostObject::~RNFSTurboHostObject() {
// remove created objects here
}
std::vector<jsi::PropNameID> RNFSTurboHostObject::getPropertyNames(jsi::Runtime& rt) {
return jsi::PropNameID::names(rt, "exists", "existsAssets", "existsRes");
}
jsi::Value RNFSTurboHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) {
std::string propName = propNameId.utf8(runtime);
// implementation here
return jsi::Value::undefined();
}
Now let's write an implementation of the simplest function - exists
. It checks the existence of a file/folder and returns true / false
.
...
jsi::Value RNFSTurboHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) {
std::string propName = propNameId.utf8(runtime);
if (propName == "exists") {
return jsi::Function::createFromHostFunction(
runtime, jsi::PropNameID::forAscii(runtime, propName),
1,
[this, propName](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
if (count != 1 || !arguments[0].isString()) [[unlikely]] {
throw jsi::JSError(runtime, "First argument ('filepath') has to be of type string");
}
std::string filePath = arguments[0].asString(runtime).utf8(runtime);
bool exists{false};
struct stat t_stat;
exists = stat(filePath.c_str(), &t_stat) == 0;
return jsi::Value(exists);
}
);
}
return jsi::Value::undefined();
}
...
Probably it would be easier to use the exists
method from the standard filesystem
library (C++17), but the old C solution is also quite functional. In my library for the rest of the methods I used the filesystem
library.
3. Platform-dependent methods (C++)
Let's consider the platform-dependent methods - existsAssets
and existsRes
. They should only be available on Android. And also consider a new method for iOS (it's not needed, but we'll consider it as an example) - existsIOS
.
To do this, first we'll add a platform-dependent module. Let's call it RNFSTurboPlatformHelper
and describe it in cpp/RNFSTurboPlatformHelper.h
:
#pragma once
class RNFSTurboPlatformHelper {
#ifdef __ANDROID__
public:
RNFSTurboPlatformHelper(JNIEnv *env);
private:
JNIEnv *jniEnv;
jobject jniObj;
#endif
#ifdef __APPLE__
public:
RNFSTurboPlatformHelper();
#endif
public:
bool existsAssetsOrRes(const char* filePath, bool isRes);
bool existsIOS(const char* filePath);
};
Here you can see the declaration of JNIEnv *jniEnv
and jobject jniObj
. These variables will be used in JNI to access the environment.
Then we add our helper to cpp/RNFSTurboHostObject.h
:
...
#ifdef __ANDROID__
#include <fbjni/fbjni.h>
#include <jni.h>
#endif
#include "RNFSTurboPlatformHelper.h"
...
private:
RNFSTurboPlatformHelper* platformHelper;
And we'll rewrite the constructor and destructor in cpp/RNFSTurboHostObject.cpp
:
RNFSTurboHostObject::RNFSTurboHostObject() {
#ifdef __ANDROID__
JNIEnv *env = facebook::jni::Environment::current();
platformHelper = new RNFSTurboPlatformHelper(env);
#endif
#ifdef __APPLE__
platformHelper = new RNFSTurboPlatformHelper();
#endif
}
RNFSTurboHostObject::~RNFSTurboHostObject() {
delete platformHelper;
platformHelper = nullptr;
}
Here we use the built-in constant (__ANDROID__
and __APPLE__
) to determine which version of PlatformHelper
we need to use.
4. Platform-dependent methods (Android)
For Android, we'll use the current JNI Environment taken from React-Native.
Let's create a basic implementation of the existsAssetsOrRes
method in android/src/main/java/com/test/rnfsturbo/RNFSTurboPlatformHelper.java
(in my library I copied this from react-native-fs
library, combining the existsAssets
and existsRes
methods):
package com.test.rnfsturbo;
import android.content.Context;
/* ... */
public class RNFSTurboPlatformHelper {
public static String tag = "RNFSTurboPlatformHelper";
private Context context;
public RNFSTurboPlatformHelper(Context ctx) {
context = ctx;
}
public boolean existsAssetsOrRes(String filePath, boolean isRes) throws Exception {
/* ... */
return true;
}
}
Now let's create a С++ implementation in Android - android/src/main/cpp/RNFSTurboPlatformHelper.cpp
:
#include <jni.h>
#include "RNFSTurboPlatformHelper.h"
RNFSTurboPlatformHelper::RNFSTurboPlatformHelper(JNIEnv *env) {
jniEnv = env;
jclass jniCls = env->FindClass("com/test/rnfsturbo/RNFSTurboPlatformHelper");
jmethodID initObject = jniEnv->GetMethodID(jniCls, "<init>", "(Landroid/content/Context;)V");
jclass contextCls = env->FindClass("com/test/rnfsturbo/RNFSTurboPlatformContextModule");
jmethodID mid = env->GetStaticMethodID(contextCls, "getContext", "()Landroid/content/Context;");
jobject context = env->CallStaticObjectMethod(contextCls, mid);
jobject obj = jniEnv->NewObject(jniCls, initObject, context);
jniObj = jniEnv->NewGlobalRef(obj);
jniEnv->DeleteLocalRef(jniCls);
jniEnv->DeleteLocalRef(context);
jniEnv->DeleteLocalRef(obj);
}
bool RNFSTurboPlatformHelper::existsAssetsOrRes(const char *filePath, bool isRes) {
jclass jniCls = jniEnv->GetObjectClass(jniObj);
jmethodID mid = jniEnv->GetMethodID(
jniCls,
"existsAssetsOrRes",
"(Ljava/lang/String;Z)Z"
);
jboolean isExists = jniEnv->CallBooleanMethod(
jniObj,
mid,
jniEnv->NewStringUTF(filePath),
isRes
);
jniEnv->DeleteLocalRef(jniCls);
if (jniEnv->ExceptionCheck()) {
jniEnv->ExceptionClear();
throw isRes ? "Failed to open asset" : "Failed to open res";
}
return (bool)isExists;
}
Here we use JNI to initialize the RNFSTurboPlatformHelper
Java class, and then to call the Java existsAssetsOrRes
method. One of the main things is to remember to delete all created links to avoid memory leaks. The rest can be found in the documentation for JNI.
It remains to add the CPP implementation in cpp/RNFSTurboHostObject.cpp
:
...
if (propName == "exists" || propName == "existsAssets" || propName == "existsRes") {
return jsi::Function::createFromHostFunction(
runtime, jsi::PropNameID::forAscii(runtime, propName),
1,
[this, propName](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
#ifndef __ANDROID__
if (propName == "existsAssets" || propName == "existsRes") {
throw jsi::JSError(runtime, "Command only for Android");
}
#endif
if (count != 1 || !arguments[0].isString()) [[unlikely]] {
throw jsi::JSError(runtime, "First argument ('filepath') has to be of type string");
}
std::string filePath = arguments[0].asString(runtime).utf8(runtime);
bool exists{false};
if (propName == "existsAssets" || propName == "existsRes") {
#ifdef __ANDROID__
exists = platformHelper->existsAssetsOrRes(filePath.c_str(), propName == "existsRes");
#endif
} else {
struct stat t_stat;
exists = stat(filePath.c_str(), &t_stat) == 0;
}
return jsi::Value(exists);
}
);
}
...
5. Platform-dependent methods (iOS)
Now let's look at PlatformHelper for iOS. It's much simpler because you don't have to deal with a different interface (like JNI) and the code is written in Objective-C++.
We want to add a new method - existsIOS
.
In this case we only need to create an implementation in ios/RNFSTurboPlatformHelper.mm
:
#import <Foundation/Foundation.h>
#import "RNFSTurboPlatformHelper.h"
RNFSTurboPlatformHelper::RNFSTurboPlatformHelper() {}
bool RNFSTurboPlatformHelper::existsIOS(const char* path) {
/* ... */
return true;
}
And that's all. Now let's finish cpp/RNFSTurboHostObject.cpp
:
...
bool exists{false};
if (propName == "existsAssets" || propName == "existsRes") {
#ifdef __ANDROID__
exists = platformHelper->existsAssetsOrRes(filePath.c_str(), propName == "existsRes");
#endif
} else if (propName == "existsIOS") {
#ifdef __APPLE__
exists = platformHelper->existsIOS(filePath.c_str());
#endif
} else {
struct stat t_stat;
exists = stat(filePath.c_str(), &t_stat) == 0;
}
...
Essentially, this is all we need to successfully implement the basic methods.
6. Working with Threads
This is just an informative part to show how to work with Threads. If you don't plan to use them, you can skip it.
The next problem appeared when I reached the downloadFile
and uploadFiles
methods. Initially, I implemented them with simple callbacks, which I called from native Java and Objective-C code.
To avoid going into details, I'll describe an example implementation of these methods via std::thread
:
...
typedef std::function<void (int jobId, int statusCode, float bytesWritten)> RNFSTurboCompleteDownloadCallback;
...
if (propName == "downloadFile") {
return jsi::Function::createFromHostFunction(
runtime, jsi::PropNameID::forAscii(runtime, propName),
1,
[this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
...
std::shared_ptr<jsi::Function> completeFunc = std::make_shared<jsi::Function>(arguments[1].asObject(runtime).asFunction(runtime));
RNFSTurboCompleteDownloadCallback completeCallback = [&runtime, completeFunc, this](int jobId, int statusCode, float bytesWritten) -> void {
jsi::Object result = jsi::Object(runtime);
result.setProperty(runtime, "jobId", jsi::Value(jobId));
result.setProperty(runtime, "statusCode", jsi::Value(statusCode));
result.setProperty(runtime, "bytesWritten", jsi::Value(bytesWritten));
completeFunc->call(runtime, std::move(result));
};
std::thread([completeCallback, &runtime]() {
completeCallback(1, 200, 100);
}).detach();
}
);
}
Here we create a C++ callback function completeFunc
from JS callback param (jsi::Function
), which will be called after the file download is complete (I used std::thread
as an example to show what happens in this case when working with threads).
And here the problem awaited me - I periodically got an application crash (since the callback sometimes returned in another thread).
After digging through the documentation and asking in React-Native Github Issue Tracker, I realized that I need to use CallInvoker
or RuntimeExecutor
to switch back to the JavaScript thread (see docs).
To do this, we'll slightly change cpp/RNFSTurboHostObject.h
:
...
public:
...
std::shared_ptr<facebook::react::CallInvoker> jsInvoker;
...
And complete cpp/NativeRNFSTurboModule.cpp
:
...
jsi::Object NativeRNFSTurboModule::createRNFSTurbo(jsi::Runtime& runtime) {
auto instance = std::make_shared<RNFSTurboHostObject>();
instance->jsInvoker = jsInvoker_;
return jsi::Object::createFromHostObject(runtime, instance);
}
...
Now we can implement callbacks through jsInvoker
in cpp/RNFSTurboHostObject.cpp
:
...
if (propName == "downloadFile") {
return jsi::Function::createFromHostFunction(
runtime, jsi::PropNameID::forAscii(runtime, propName),
1,
[this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
...
std::shared_ptr<jsi::Function> completeFunc = std::make_shared<jsi::Function>(arguments[1].asObject(runtime).asFunction(runtime));
RNFSTurboCompleteDownloadCallback completeCallback = [&runtime, completeFunc, this](int jobId, int statusCode, float bytesWritten) -> void {
jsInvoker->invokeAsync([&runtime, completeFunc, jobId, statusCode, bytesWritten]() {
jsi::Object result = jsi::Object(runtime);
result.setProperty(runtime, "jobId", jsi::Value(jobId));
result.setProperty(runtime, "statusCode", jsi::Value(statusCode));
result.setProperty(runtime, "bytesWritten", jsi::Value(bytesWritten));
completeFunc->call(runtime, std::move(result));
});
};
std::thread([completeCallback, &runtime]() {
completeCallback(1, 200, 100);
}).detach();
}
);
}
...
That's it, the app didn't crash anymore.
7. TypeScript module
Now let's prepare the TypeScript part. Create a file src/NativeRNFSTurboModule.ts
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
import {UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes';
import {getRNFSTurboPlatformContextTurboModule} from './NativeRNFSTurboPlatformContextModule';
export interface Configuration {
documentDirectoryPath: string;
/* ... */
}
export interface Spec extends TurboModule {
/**
* Create a new instance of RNFSTurbo.
* The returned {@linkcode UnsafeObject} is a `jsi::HostObject`.
*/
readonly createRNFSTurbo: () => UnsafeObject;
}
let module: Spec | null;
let configuration: Configuration;
export function getRNFSTurboModule(): {
configuration: Configuration;
module: Spec;
} {
try {
if (module == null) {
// 1. Load RNFS TurboModule
module = TurboModuleRegistry.getEnforcing<Spec>('RNFSTurboModule');
// 2. Get the PlatformContext TurboModule as well
const platformContext = getRNFSTurboPlatformContextTurboModule();
// 3. Initialize it with the storage directores from platform-specific context
configuration = {
documentDirectoryPath: platformContext.getDocumentDirectoryPath(),
/* ... */
};
}
return {configuration, module};
} catch {
// TurboModule could not be found!
throw new Error('Module not found');
}
}
Then the main file src/index.ts
:
import {
getRNFSTurboModule,
type Configuration,
} from "./NativeRNFSTurboModule";
export interface RNFSTurboInterface {
readonly DocumentDirectoryPath: string;
/* ... */
exists(filepath: string): boolean;
existsAssets(filepath: string): boolean;
existsRes(filepath: string): boolean;
}
const createRNFSTurbo = (): {
configuration: Configuration;
instance: RNFSTurboInterface;
} => {
const { configuration, module } = getRNFSTurboModule();
const instance = module.createRNFSTurbo() as RNFSTurboInterface;
if (__DEV__) {
if (typeof instance !== "object" || instance == null) {
throw new Error(
"Failed to create RNFSTurbo instance - an unknown object was returned by createRNFSTurbo(..)!",
);
}
}
return { configuration, instance };
};
/**
* A single RNFSTurbo instance.
*/
class RNFSTurbo implements RNFSTurboInterface {
private nativeInstance: RNFSTurboInterface;
private functionCache: Partial<RNFSTurboInterface>;
private configuration: Configuration;
/**
* Creates a new RNFSTurbo instance with the given Configuration.
* If no custom `id` is supplied, `'rnfsturbo'` will be used.
*/
constructor() {
const { configuration, instance } = createRNFSTurbo();
this.nativeInstance = instance;
this.configuration = configuration;
this.functionCache = {};
}
private getFunctionFromCache<T extends keyof RNFSTurboInterface>(
functionName: T,
): RNFSTurbo[T] {
if (this.functionCache[functionName] == null) {
this.functionCache[functionName] = this.nativeInstance[functionName];
}
return this.functionCache[functionName] as RNFSTurbo[T];
}
get DocumentDirectoryPath(): string {
return this.configuration.documentDirectoryPath;
}
/* ... */
exists(filepath: string): boolean {
const func = this.getFunctionFromCache("exists");
return func(filepath);
}
existsAssets(filepath: string): boolean {
const func = this.getFunctionFromCache("existsAssets");
return func(filepath);
}
existsRes(filepath: string): boolean {
const func = this.getFunctionFromCache("existsRes");
return func(filepath);
}
}
const RNFSTurboInstance = new RNFSTurbo();
export default RNFSTurboInstance;
I don't think I need to explain what's going on here. But I'd like to draw your attention to a few things.
I used the same getFunctionFromCache
method from the react-native-mmkv
library, which caches the native function to have quick access to it. I haven't measured whether this increases speed, but in theory it should work faster when frequently requesting the same methods.
Next, we create an instance of the RNFSTurbo
class, which we export. When importing into the app, this single instance will always be used to avoid re-creating the class in memory.
8. Android Module description
First, create a react-native.config.js
file
module.exports = {
dependency: {
platforms: {
/**
* @type {import('@react-native-community/cli-types').IOSDependencyParams}
*/
ios: {},
/**
* @type {import('@react-native-community/cli-types').AndroidDependencyParams}
*/
android: {
cxxModuleCMakeListsModuleName: 'test-turbo-module',
cxxModuleCMakeListsPath: 'CMakeLists.txt',
cxxModuleHeaderName: 'NativeRNFSTurboModule',
},
},
},
};
Here we specify the bundle for Android and some C++ instructions.
Then, create android/build.gradle
:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.2.1"
}
}
apply plugin: "com.android.library"
apply plugin: "com.facebook.react"
android {
defaultConfig {
compileSdkVersion 31
minSdkVersion 21
}
buildFeatures {
buildConfig true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
java.srcDirs += [
// This is needed to build Kotlin project with NewArch enabled
"${project.buildDir}/generated/source/codegen/java"
]
}
}
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation "com.facebook.react:react-native:+"
}
react {
jsRootDir = file("../src/")
libraryName = "RNFSTurbo"
codegenJavaPackageName = "com.test.rnfsturbo"
}
Please note that you need to use actual versions of packages and your bundle name.
Add android/src/main/AndroidManifest.xml
:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.rnfsturbo">
</manifest>
And then create android/CMakeLists.txt
file:
cmake_minimum_required(VERSION 3.9.0)
project(RNFSTurbo)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD 17)
# Compile sources
add_library(
test-turbo-module
SHARED
src/main/cpp/RNFSTurboPlatformHelper.cpp
../cpp/RNFSTurboHostObject.cpp
../cpp/NativeRNFSTurboModule.cpp
)
# Add headers search paths
target_include_directories(test-turbo-module PUBLIC ../cpp)
# Add android/log dependency
find_library(log-lib log)
target_link_libraries(
test-turbo-module
${log-lib} # <-- Logcat logger
android # <-- Android JNI core
react_codegen_RNFSTurboSpec # <-- Generated Specs from CodeGen
)
CMakeLists.txt
contains instructions for the C++ compiler. Use your own package name and file names.
9. iOS Module description
Create a RNFSTurbo.podspec
in the root of package:
require "json"
package = JSON.parse(File.read(File.join(__dir__, "./package.json")))
Pod::Spec.new do |s|
s.name = "RNFSTurbo"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "12.4" }
s.author = package["author"]
s.source = { :git => "https://github.com/path/to/source.git", :tag => "#{s.version}" }
s.source_files = [
"ios/**/*.{h,m,mm}",
"cpp/**/*.{hpp,cpp,c,h}",
]
s.compiler_flags = '-x objective-c++'
s.pod_target_xcconfig = {
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
install_modules_dependencies(s)
end
There are almost no differences from the usual podspec
file, except that we indicate compiler_flags
and pod_target_xcconfig
.
Then create ios/RNFSTurboOnLoad.mm
file:
#import <Foundation/Foundation.h>
#import "NativeRNFSTurboModule.h"
#import <ReactCommon/CxxTurboModuleUtils.h>
@interface RNFSTurboOnLoad : NSObject
@end
@implementation RNFSTurboOnLoad
+ (void)load {
facebook::react::registerCxxModuleToGlobalModuleMap(
std::string(facebook::react::NativeRNFSTurboModule::kModuleName),
[&](std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
return std::make_shared<facebook::react::NativeRNFSTurboModule>(jsInvoker);
});
}
@end
It will register the RNFSTurbo
module in the React-Native application's module registry.
10. Creating a package.json
{
"name": "test-turbo-module",
"version": "0.0.1",
"description": "Test React-Native C++ library",
"react-native": "src/index",
"source": "src/index",
"license": "MIT",
"scripts": {
"typescript": "tsc --noEmit"
},
"repository": {
"type": "git",
"url": "git+https://github.com/path/to/source.git"
},
"homepage": "https://github.com/path/to/source",
"author": "John Doe <john@doe.com>",
"codegenConfig": {
"name": "RNFSTurboSpec",
"type": "modules",
"jsSrcsDir": "src"
},
"devDependencies": {
"react": "^18.3.1",
"react-native": "^0.74.2",
"typescript": "^5.1.6"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
}
The final package structure will look like this:
Adding a library to your application
Now we can add the package test-turbo-module
to the package.json
in your application:
...
"test-turbo-module": "file:./packages/test-turbo-module"
...
And import the module in your application code:
import RNFSTurbo from 'test-turbo-module';
const fileExists = RNFSTurbo.exists(`${RNFSTurbo.DocumentDirectoryPath}/..`);
Conclusion
I can't say that everything is written perfectly, especially in terms of C++ code (since I started studying it quite recently and I don't have good practice in working with it).
But nevertheless, I got a lot of experience working on this library. In our company, we use it in production on one of the projects and it works more than stably.
I hope this article will help other developers understand C++ React-Native modules and start writing new fast and cross-platform libraries (or rewriting old and abandoned ones).
You can check the full library code here - https://github.com/cmpayc/react-native-fs-turbo
Top comments (0)