Patnaik's Lab

Kotlin Multiplatform — React Native Bridge Modules — Part I

React Native allows you to write Native Modules for exposing Platform specific functionality to JavaScript. When you use Camera or Networking or anything which interacts with Platform APIs (Android SDK or Foundation Framework), you are using Native Modules.

This is geared towards all developers, so if you have iOS / RN experience feel free to skip the first section. If you are also aware of the macro’s internals and how React Native Modules work, skip the second section.

This is Part 1 where we understand how the modules work and we find a way to write modules which is transferable to Kotlin (and Swift).

The Part 2 of this article is separate because of length and semantic reasons. It will write the same Native Module in Kotlin.

There will be links to articles and source code (including react source code) where appropriate.

This is written like a textbook chapter and I dive into considerable depth, so please let me know your feedback in the comments.

I do not just want you to learn the concepts, I want you to learn how to figure out the concept. Like a treasure hunt.

Introduction

There are two main ways of writing Native Modules. Bridge and JSI.

Bridge Native Modules are the legacy way of communication from Native (Java/Objective-C) to JavaScript. It uses JSON for serializing arguments and results between the two realms.

JSI stands for JavaScript Interface, and is the modern alternate to Bridge. It works by exposing a common interface from every JS engine (hermes/JSC/etc) to C++. You can now write C++ to perform alll sorts of manipulations on the running JavaScript code.

To understand these in greater depth you can have a look at my other article Cross Platform Tech and also there are tons of resources on the basics.

Implementation

In this article we will be writing a Bridge Module for Kotlin Multiplatform.

Writing one for Android is very easy, we will cover it at the last. Writing one for iOS in Kotlin is substantially more tricky. We will be first writing that.

First we will write a basic one in Objective-C. While Objective-C modules use macros to register the Native Module, Kotlin has no concept of macros. We will dive deep into how the macros work.

Second we write the same Native Module without macros. This will let us replace the magic of preprocessors with normal code.

Third we write the Native Module in Kotlin. We will use cinterop to expose the interfaces RCTBridgeModule and RCTBridgeMethod to Kotlin, then we will use a different way of registering these modules.

Let’s start.

General Setup

I am using React Native 0.73.4.

Don’t worry, the concepts are applicable for other versions too. Now to start, run this in your favourite shell.

git clone https://github.com/shibasis0801/KotlinBridgeModule.git

This project was generated using the command. (you don’t need to execute this).

npx [email protected] init KotlinBridgeModule --version 0.73.4

I am using Android Studio Iguana and Xcode 15 to run these. You also also install the Kotlin Multiplatform Plugin for Android Studio for better development experience. There is also the Kotlin Plugin for XCode but I haven’t used that yet. (will update if I do)

You can add an iOS run target to Android Studio this way. (Not necessary)

iOS Run Target (optional)

Add a run configuration with these details (the target can be any device)

Running the App

Now open the ios folder in Xcode, android in Android Studio and the root folder in VSCode ( :/ , Maybe once Jetbrains Fleet is stable this won’t require 3 IDEs)

Start the metro bundler in one terminal, and run both apps.

You should have this screen. Our goal is Minimal UI and Maximum Segfaults ( hehe ).

First — iOS Bridge Module — Objective-C

Switch to git branch “start” for this section.

git checkout start

A Bridge Module supports three types of functions ( Source Code ).

  1. Normal Async Functions: If you need a result, you must pass a callback.
  2. Sync Blocking Functions: These are like normal functions which optionally return a result.
  3. Promise Functions: Return a Promise

I feel that a mathematical way of building things from bottom up makes sense when you are building systems. Understanding systems on the other hands works great when top down.

Start

We will write our bridge module similarly, one function of each type to ensure we are covering most usecases.

Create two files named NormalBridgeModule.h and NormalBridgeModule.m using Xcode.

It should look like this.

Clear all this and add the import header directive in your implementation file (.m)

Now add the declaration for your module in the header

#import <React/RCTBridgeModule.h>

@interface NormalBridgeModule : NSObject<RCTBridgeModule>
@end

Add the basic implementation for this interface in NormalBridgeModule.m

#import "NormalBridgeModule.h"

@implementation NormalBridgeModule

RCT_EXPORT_MODULE();

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(syncBlockingFunction)
{
return @"Result from syncBlockingFunction";
}

@end

The macros RCT_EXPORT_MODULE and RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD are critical for functioning. Their working will be explained in later sections.

Apart from that, this function just returns a NSString

Your code should look like this

Now add this in your AppDelegate.mm file

#import "NormalBridgeModule.h"

Rebuild the App.

Add code in App.tsx

import { NativeModules } from 'react-native';
const { NormalBridgeModule } = NativeModules;
console.log("NormalBridgeModule", NormalBridgeModule);

Your app should reload, and you should be seeing this in the logs

Just below <Text style={styles.text}> Sync Blocking Method </Text>, add this

<Text style={styles.text}> {NormalBridgeModule?.syncBlockingFunction()} </Text>

Your app should show the result

Great !!!

Callback Function

Now, writing the one with callbacks.

Add this block to NormalBridgeModule.m.

We receive a callback from React in the form of RCTResponseSenderBlock. It accepts an NSArray.

We schedule an async task on GCD Queue ( task queues available on iOS for multithreading ). It waits for 2 seconds before firing the callback.

The macro RCT_EXPORT_METHOD is critical again. This is used for non-blocking methods.

RCT_EXPORT_METHOD(normalAsyncFunction:(RCTResponseSenderBlock)callback)
{
// Perform some asynchronous operation
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Simulate a delay
[NSThread sleepForTimeInterval:2.0];

// Callback with some result
callback(@[@"Result from normalAsyncFunction"]);
});
}

Rebuild. We can see the new function in logs.

Add the following code in your App.tsx

  const [ callbackResult, setCallbackResult ] = useState('');
useEffect(() => {
NormalBridgeModule?.normalAsyncFunction(setCallbackResult)
}, [])

And in the render section

<View>
<Text style={styles.text}> Callback Method </Text>
<Text style={styles.text}> {callbackResult} </Text>
</View>

We can see our app

Promise Function

Now add this section for the final function ( Promise ).

We receive two callbacks, resolve (RCTPromiseResolveBlock) and reject (RCTPromiseRejectBlock).

Again we sleep for two seconds and then call resolve.

RCT_EXPORT_METHOD(promiseFunction:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2.0];

resolve(@"Result from promiseFunction");

// In case of an error, you can reject the promise
// reject(@"error_code", @"Error message", error);
});
}

Rebuild.

Add the following code to your App.tsx

  const [ promiseResult, setPromiseResult ] = useState('');
useEffect(() => {
NormalBridgeModule?.promiseFunction().then(setPromiseResult)
}, [])

And in the render section

<View>
<Text style={styles.text}> Promise Method </Text>
<Text style={styles.text}> {promiseResult} </Text>
</View>

Our basic Native Module is complete

The final code is available in the branch “startMacroFree”

Second — iOS Bridge Module —Macro Free

If you skipped the earlier section, checkout the repo for this stage.

git checkout startMacroFree

There are 3 macros that were used in writing the bridge module.

  1. RCT_EXPORT_MODULE
  2. RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD
  3. RCT_EXPORT_METHOD

RCT_EXPORT_MODULE

This macro is responsible to register your Native Module with the React Framework.

Let’s see the source code for it.

/**
* Place this macro in your class implementation to automatically register
* your module with the bridge when it loads. The optional js_name argument
* will be used as the JS module name. If omitted, the JS module name will
* match the Objective-C class name.
*/
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+(NSString *)moduleName \
{ \
return @ #js_name; \
} \
+(void)load \
{ \
RCTRegisterModule(self); \
}

Hmm, this looks tricky if you have less experience with Macros. But there are some simple parts.

Let’s break this down.
Apart from the RCT_EXTERN thing, rest seems normal ( for a Objective-C dev ).

First we have a forward declaration as the containing header may be imported later.

void RCTRegisterModule(Class);

Next we have a method to return the name, where #js_name just replaces with actual during preprocessor phase.

+(NSString *)moduleName
{
return @ #js_name;
}

Next we have the special load method which is called when this class is loaded into memory.

+(void)load
{
RCTRegisterModule(self);
}

Aha, this is why just importing the header adds the Native Module to React. Importing loads the class, on load Register is called.

This is the benefit of reading framework source code.

Now RCT_EXTERN.

 /**
* Make global functions usable in C++
*/
#if defined(__cplusplus)
#define RCT_EXTERN extern "C" __attribute__((visibility("default")))
#define RCT_EXTERN_C_BEGIN extern "C" {
#define RCT_EXTERN_C_END }
#else
#define RCT_EXTERN extern __attribute__((visibility("default")))
#define RCT_EXTERN_C_BEGIN
#define RCT_EXTERN_C_END
#endif

I don’t understand what this is doing exactly. Okay time to invoke our machine overlords. GPT(or Gemini) is extraordinary for understanding things if you also have the source of truth and do not treat it as gospel.

Let’s see what GPT Supreme has to say,

Okay, if defined was basic.

If you have written mixed C/C++ code earlier then you are aware of name mangling, where C++ mangles code imported from C (as C lacks namespaces and function overloading).

C_BEGIN and C_END also seem basic.

__attribute__ ((visibility(“default”))). Hmm. Let’s ask again.

Okay, so like other languages have Public/Private access modifiers, GCC has these attributes to make sections public and private. (Default header imports make everything public).

Since our code is in the user facing app, we don’t care much about inheritance access. We can ignore this.

Replacing RCT_EXPORT_MODULE

We want to achieve the same functionality, but without this macro.

Replace the line with the Macro with this

void RCTRegisterModule(Class);
+(NSString *)moduleName
{
return @"NormalBridgeModule";
}
+(void)load
{
RCTRegisterModule(self);
}

Your code should now look like this

Rebuild and it should work fine.

Replace RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD and RCT_EXPORT_METHOD

To understand these two macros is in a league higher. There is a lot of tooling that connect methods to react.

These two macros are actually a specialization of the macro _RCT_EXTERN_REMAP_METHOD

#define _RCT_EXTERN_REMAP_METHOD(js_name, method, is_blocking_synchronous_method)                            \
+(const RCTMethodInfo *)RCT_CONCAT(__rct_export__, RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) \
{ \
static RCTMethodInfo config = {#js_name, #method, is_blocking_synchronous_method}; \
return &config; \
}

This declares a method with the prefix of __rct_export__ and suffix of a unique numeric string.

for example if the method name supplied was getData, the final name would be something like

__rct_export__getData232

Why ?

Objective-C does not support function overloading like C++ does. In order to increase chances of uniqueness, we have the suffix. (I do feel there may bepossiblity of collision but for another article. )

Okay, why the suffix ?

Good question, diving back into source. If you open the source code in github.dev, it becomes very easy to browse (cloning makes it even easier).

But for now, we will do github search.

In order to ensure our findings are reality, we need to attach debugger in XCode to these files, and check if it is actually getting called.

We can find it in our Pods. We put a debugger in the block.

We run our code again.

We will get lots of pauses as this is in the critical sections, with a little patience skip the irrelevant items. And after sometime, you will reach gold.

Here we can see the debugger is paused for normalAsyncFunction. We also see that the selector has __rct_export__181. Where’s our name ?

But we never supplied it in js_name, so the selector is {prefix}{""}{suffix}
There’s no magic, there is just code. Unless it is a song by K.K., that was magic.

Now we can see that the method metadata is used to fetch the method and store it in some array called module methods.
But there is no invokation here also. Hmm.

There is something called RCTBridgeMethod here. That looks promising, but it is not invoked anywhere.

What do we do ?

Well if the method stored is a RCTBridgeMethod, the method being called also would be the same right ?

We need to find where this is called.

A search for RCTBridgeMethod brings a lot of results. No, brute searches won’t work. We need to be clever.

Let’s see what this RCTBridgeMethod is.

@protocol RCTBridgeMethod <NSObject>

@property (nonatomic, readonly) const char *JSMethodName;
@property (nonatomic, readonly) RCTFunctionType functionType;

- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments;

@end

Okay, this is a protocol. And a simple one too. It has one method invokeWithBridge which is highly likely to be the one being called when there is an invokation.

Brilliant.

Let’s search for “invokeWithBridge”

We have multiple results but not too many. There are tests, UI managers, Turbo Modules (for another article), and our prize, RCTNativeModule

Let’s put a debugger and verify if our guess is correct.

Using XCode file filter, we open the file and attach the debugger

A hit for syncBlockingFunction

Let’s step over, and check the result

Awesome !! We have our result there.

The result is then converted to a folly::dynamic (you don’t need to worry about it right now) and then returned.

The function inside which this code is present is invokeInner.

We want to know who calls this.

How ?

By now, it should be Elementary, my dear Watson

We search and attach a debugger.

The result is just above the function, callSerializableNativeHook

Who calls this ?

In the debugger, we can see the call stack.

And so we have reached the Bridge. Not so mysterious anymore. ( For a higher level overview, watch Parashuram React 2018 and read Cross Platform Tech )

What did the macros do ?

The macros captured metadata and ensured uniqueness, so that the actual methods can be constructed and injected into an array. The React Framework then pulls our method and invokes it when needed.

So how can we replace them ?

We need to create the RCTBridgeMethod and then inject it the React Module Methods array.

Inserting seems to be a question of accessing the array. Creating the RCTBridgeMethod seems tricky, we will do that first.

Again, why do we need to replace them ?

To understand the magic behind the macros, and to write Native Modules in Swift / Kotlin where there are no macros, without having bindings in Objective-C.

Implementing the Sync Method in RCTBridgeMethod

Let’s have a look at the protocol again.

@protocol RCTBridgeMethod <NSObject>

@property (nonatomic, readonly) const char *JSMethodName;
@property (nonatomic, readonly) RCTFunctionType functionType;

- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments;

@end

This seems to contain a method invokeWithBridge which should contain our functionality. The other two seem to be metadata.
The module is passed as an argument, followed by the actual arguments.

Let’s convert the simplest method we wrote to a raw RCTBridgeMethod.

// Original
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(syncBlockingFunction)
{
return @"Result from syncBlockingFunction";
}
@interface SyncBlockingFunction : NSObject <RCTBridgeMethod>
@end

@implementation SyncBlockingFunction

@synthesize JSMethodName = _JSMethodName;
@synthesize functionType = _functionType;

- (instancetype)init {
self = [super init];
if (self) {
_JSMethodName = "syncBlockingFunction";
_functionType = RCTFunctionTypeSync;
}
return self;
}

- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments {
return @"NormalBridgeModule";
}

@end

As we learnt in the beginning, there are 3 types of methods that are supported, blocking sync, async callback and promise.

We set the appropriate name and functionType.

We then move our code inside the invokeWithBridge method.

Nice. But will this run ?

No. We still need to figure out how to link it to the native module.
The RCTBridgeModule is a larger interface, but not too big. Read the file once. You will find, that it has a method methodsToExport which seems appropriate for us.

We add this to our NormalBridgeModule

- (NSArray<id<RCTBridgeMethod>> *)methodsToExport {
return @[
[SyncBlockingFunction new]
];
}

We rebuild and rerun the app. It should be working as usual meaning our changes have worked.

Moving on.

Implementing the Callback Method in RCTBridgeMethod

We write a similar implementation

@interface CallbackFunction : NSObject <RCTBridgeMethod>
@end

@implementation CallbackFunction
@synthesize JSMethodName = _JSMethodName;
@synthesize functionType = _functionType;
- (instancetype)init {
self = [super init];
if (self) {
_JSMethodName = "normalAsyncFunction";
_functionType = RCTFunctionTypeNormal;
}
return self;
}

- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments {
// no safety code present for bounds and type checks.
RCTResponseSenderBlock callback = arguments[0];
callback(@[@"Result from normalAsyncFunction"]);
return nil;
}
@end

But alas, this throws invalid access error. Is it due to the missing bounds check ? No, because the callback should be there.

Is it ? No.

Clearly we are missing something when parameters are involved.

Let’s go back to the source.

This time, we don’t have a pointer to search for.
Is this the end of our journey ?
Do we give up ?
No, we do not.

When I can’t find pointers, I try to find tests. These frameworks are used by large companies and an incredible amount of business depends on their functioning. Almost always they will have tests.

And the benefit of tests is, it will be written with the least amount of code needed to just verify. User level code will contain lots of moving pieces, unit tests will be concise.

If you go back to the macro expansion, __rct_export__, Method Metadata were present in order to invoke methods. We didn’t use them because the simple function worked without. But they can’t be useless.

In the unit tests, we can find a test for RCTModuleMethod which uses the Method Metadata. Maybe finally, we can figure this out.
The test file is RCTModuleMethodTests

Here we see

static RCTModuleMethod *buildDefaultMethodWithMethodSignature(const char *methodSignature)
{
// This leaks a RCTMethodInfo, but it's a test, so...
RCTMethodInfo *methodInfo = new RCTMethodInfo{.objcName = methodSignature, .isSync = NO};
return [[RCTModuleMethod alloc] initWithExportedMethod:methodInfo moduleClass:[RCTModuleMethodTests class]];
}

Bingo, we can now build our method using this.

We actually found a great class. This not only solves our issue, but also contains an entire Native Module written with minimal macros. It will help us verify our findings further.

What RCTModuleMethod seems to do is instead of having to write a lot of code for one method when we were using RCTBridgeMethod, we can just wrap an existing method with some metadata.

It is a subclass of RCTBridgeMethod so it will also result in creating an RCTBridgeMethod object, just that it would be a little simpler for the dev. ( we can go into details to see how it works too )

Attempt 2: Implementing the Callback Method with RCTModuleMethod

Now, first things first, this code is using new/delete which is C++. We need to rename the file from NormalBridgeModule.m to NormalBridgeModule.mm (with Xcode).

Add few headers

#import <React/RCTBridge.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridgeMethod.h>
#import <React/RCTModuleMethod.h>

Now we define our method as a normal objective c method


- (void)normalAsyncFunction:(RCTResponseSenderBlock)callback
{
callback(@[ @"Result from normalAsyncFunction" ]);
}

I removed all code for delay because our focus currently is on callback functioning.

This function is straightforward. How do we expose it.
Following the patterns shown in RCTModuleMethodTests

We create Method Metadata

const char *normalAsyncFunctionSignature = "normalAsyncFunction:(RCTResponseSenderBlock)callback";
// this will leak
RCTMethodInfo *methodInfo = new RCTMethodInfo{.objcName = normalAsyncFunctionSignature, .isSync = NO};

This is also simple, we write the selector syntax. Then we store it in methodInfo with other metadata.

An important thing here is, we are allocating the metadata on the heap.
This is done because the metadata is needed at later stages, and throughout the lifetime of the module.
We are not deleting it anywhere, which should be fine as neither does the module get cleared.

Still, we should dive into how exactly this metadata is used, and find an alternate.

We build the method using the RCTModuleMethod initializer.

id callbackMethod = [[RCTModuleMethod alloc] initWithExportedMethod:methodInfo moduleClass:[self class]];

We add it to the array, the final function now looks like this

- (NSArray<id<RCTBridgeMethod>> *)methodsToExport {
const char *normalAsyncFunctionSignature = "normalAsyncFunction:(RCTResponseSenderBlock)callback";
RCTMethodInfo *methodInfo = new RCTMethodInfo{.objcName = normalAsyncFunctionSignature, .isSync = NO};
id callbackMethod = [[RCTModuleMethod alloc] initWithExportedMethod:methodInfo moduleClass:[self class]];


return @[
[SyncBlockingFunction new],
callbackMethod
];
}

We rebuild. It should work as usual.

Phew.

Implementing the Promise Method in RCTModuleMethod

This seems to be very similar to how we implemented the callback method. Except with two callbacks.
Let’s test out the theory.

We write the promise Function


-(void)promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
resolve(@"Result from promiseFunction");
}

We add the following code to methodsToExport function

 const char *promiseFunctionSelector = "promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject";
RCTMethodInfo *promiseMethodInfo = new RCTMethodInfo{.objcName = promiseFunctionSelector, .isSync = NO};
id promiseMethod = [[RCTModuleMethod alloc] initWithExportedMethod:promiseMethodInfo moduleClass:[self class]];

We add the method to the array.

The final function looks like this, ( I have renamed few fields to make sense )

- (NSArray<id<RCTBridgeMethod>> *)methodsToExport {
const char *normalAsyncFunctionSelector = "normalAsyncFunction:(RCTResponseSenderBlock)callback";
RCTMethodInfo *callbackMethodInfo = new RCTMethodInfo{.objcName = normalAsyncFunctionSelector, .isSync = NO};
id callbackMethod = [[RCTModuleMethod alloc] initWithExportedMethod:callbackMethodInfo moduleClass:[self class]];

const char *promiseFunctionSelector = "promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject";
RCTMethodInfo *promiseMethodInfo = new RCTMethodInfo{.objcName = promiseFunctionSelector, .isSync = NO};
id promiseMethod = [[RCTModuleMethod alloc] initWithExportedMethod:promiseMethodInfo moduleClass:[self class]];


return @[
[SyncBlockingFunction new],
callbackMethod,
promiseMethod
];
}

Let’s run it. It should work normally.

Let’s have a look at the entire file.

#import "NormalBridgeModule.h"
#import <React/RCTBridge.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridgeMethod.h>
#import <React/RCTModuleMethod.h>


@interface SyncBlockingFunction : NSObject <RCTBridgeMethod>
@end

@implementation SyncBlockingFunction

@synthesize JSMethodName = _JSMethodName;
@synthesize functionType = _functionType;
- (instancetype)init {
self = [super init];
if (self) {
_JSMethodName = "syncBlockingFunction";
_functionType = RCTFunctionTypeSync;
}
return self;
}

- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments {
return @"NormalBridgeModule";
}
@end

@implementation NormalBridgeModule
void RCTRegisterModule(Class);
+(NSString *)moduleName
{
return @"NormalBridgeModule";
}
+(void)load
{
RCTRegisterModule(self);
}

- (void)normalAsyncFunction:(RCTResponseSenderBlock)callback
{
callback(@[ @"Result from normalAsyncFunction" ]);
}

- (void)promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
resolve(@"Result from promiseFunction");
}

- (NSArray<id<RCTBridgeMethod>> *)methodsToExport {
const char *normalAsyncFunctionSelector = "normalAsyncFunction:(RCTResponseSenderBlock)callback";
RCTMethodInfo *callbackMethodInfo = new RCTMethodInfo{.objcName = normalAsyncFunctionSelector, .isSync = NO};
id callbackMethod = [[RCTModuleMethod alloc] initWithExportedMethod:callbackMethodInfo moduleClass:[self class]];

const char *promiseFunctionSelector = "promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject";
RCTMethodInfo *promiseMethodInfo = new RCTMethodInfo{.objcName = promiseFunctionSelector, .isSync = NO};
id promiseMethod = [[RCTModuleMethod alloc] initWithExportedMethod:promiseMethodInfo moduleClass:[self class]];


return @[
[SyncBlockingFunction new],
callbackMethod,
promiseMethod
];
}

@end

We have no macros now, and we can transfer this code to Swift and Kotlin very easily.
Also we should do something about the SyncBlockingFunction. It should be possible to write with RCTModuleMethod.

You know the drill

Write the method

- (NSString *)syncBlockingFunction {
return @"NormalBridgeModule";
}

Create the metadata and use it to create the RCTModuleMethod

 const char *syncBlockingSelector = "syncBlockingFunction";
RCTMethodInfo *syncMethodInfo = new RCTMethodInfo{.objcName = syncBlockingSelector, .isSync = YES};
id syncMethod = [[RCTModuleMethod alloc] initWithExportedMethod:syncMethodInfo moduleClass:[self class]];

Add it to the array

  return @[
syncMethod,
callbackMethod,
promiseMethod
];

It should work normally.

Finally we have a macro-free bridge Native Module,

@implementation NormalBridgeModule
void RCTRegisterModule(Class);
+(NSString *)moduleName
{
return @"NormalBridgeModule";
}
+(void)load
{
RCTRegisterModule(self);
}
- (NSString *)syncBlockingFunction {
return @"NormalBridgeModule";
}

- (void)normalAsyncFunction:(RCTResponseSenderBlock)callback
{
callback(@[ @"Result from normalAsyncFunction" ]);
}

- (void)promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
resolve(@"Result from promiseFunction");
}

- (NSArray<id<RCTBridgeMethod>> *)methodsToExport {
const char *syncBlockingSelector = "syncBlockingFunction";
RCTMethodInfo *syncMethodInfo = new RCTMethodInfo{.objcName = syncBlockingSelector, .isSync = YES};
id syncMethod = [[RCTModuleMethod alloc] initWithExportedMethod:syncMethodInfo moduleClass:[self class]];

const char *normalAsyncFunctionSelector = "normalAsyncFunction:(RCTResponseSenderBlock)callback";
RCTMethodInfo *callbackMethodInfo = new RCTMethodInfo{.objcName = normalAsyncFunctionSelector, .isSync = NO};
id callbackMethod = [[RCTModuleMethod alloc] initWithExportedMethod:callbackMethodInfo moduleClass:[self class]];

const char *promiseFunctionSelector = "promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject";
RCTMethodInfo *promiseMethodInfo = new RCTMethodInfo{.objcName = promiseFunctionSelector, .isSync = NO};
id promiseMethod = [[RCTModuleMethod alloc] initWithExportedMethod:promiseMethodInfo moduleClass:[self class]];


return @[
syncMethod,
callbackMethod,
promiseMethod
];
}

@end

Thanks a ton for reading the entire article. It takes a great amount of patience and curiosity to go through this deep dive. Now framework source code should no longer be too intimidating. Give yourself a well deserved pat on the back.

The final repo is available in the “final” branch. https://github.com/shibasis0801/KotlinBridgeModule/tree/final

I hope that I was able to explain it properly. If you have any doubts, do ask in the comment section.

The next article will be on writing this same native module in Kotlin/Native.

Thanks again.
Shibasis.