Patnaik's Lab

Kotlin Multiplatform — React Native Bridge Modules — Part II

This is the part two of my series on writing React Native Bridge Modules for Kotlin Multiplatform.

The earlier article is required reading for this one.
In Part I, we write a simple bridge module with Objective-C, understand the macros being used, then write the same module without macros.

A macro free version is very important because it lets us write Native Modules in other languages like Swift and Kotlin which can interface with Objective-C, without having to write Objective-C wrappers on top.

Kotlin Multiplatform — React Native Bridge Modules

To start, clone the repo and checkout the branch kotlin-start.

git clone https://github.com/shibasis0801/KotlinBridgeModule.git
git checkout kotlin-start

This repo has the following structure.

The android and ios apps are from the react native template.
The kotlin-bridge is the shared kotlin code between android and iOS which implements the bridge native module.

This isn’t the best structure, but is used to make the tutorial easier and not have to deal with nested gradle builds.
In real world, the kotlin-bridge library should be an independent gradle project outside of both apps.

Clone the repo, open the android and iOS folders in respective IDEs.

In Android Studio, your code should look like this

Xcode should be looking like this

Okay, we can see the Native Module we wrote in the previous article.

Recap

To recap and keep this article self contained, let us revisit the Native Module and understand what it does.

If you have done Part I, please skip this section.

https://medium.com/media/886cc42bad8d74d3ea124d63e6327dd2/href

Let’s break this down.

Normal Bridge Module is an RCTBridgeModule, which means a React Bridge Module.

We have a forward declaration for RegisterModule. This lets you use the function without worrying about import order.

void RCTRegisterModule(Class);

Next we need to override a method to return the name of this module

+(NSString *)moduleName {
return @"NormalBridgeModule";
}

Next we register this module with React Framework when this class is loaded into memory.

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

Next, we have a function which is synchronous and blocking, and returns a value.

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

Next, we have a function which takes a callback, and sends a result through it

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

Next, we have a function which takes two promise blocks for resolving and rejecting and we resolve a value in it.


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

Finally we register these methods and attach them to React framework.

- (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
];
}

If you need to understand how these work in depth, please do read Part I

Kotlin Multiplatform — React Native Bridge Modules

Integrating the Kotlin Library with our iOS app

Now, we are ready to integrate the Kotlin Library with our iOS app.

Open Android Studio, and open KotlinBridgeModule.kt

Right now, it does not do much. But has the basic skeleton we need.

To ensure everything is fine, in Android Studio, build the kotlin module once.

In your gradle window, you should have this

Once you have verified it is working fine, go to the iOS folder

Add this line to your Podfile if it is not already present.

  pod "kotlin_bridge", :path => "../android/kotlin-bridge"

Next, run pod install

Next, import this header in your AppDelegate.mm if it does not exist.

#import <kotlin_bridge/kotlin_bridge.h>

This is the same name we provided in build.gradle.kts.
Hit build. Your app should run fine.

But we haven’t actually done anything yet.

Verifying that we can access the Module

Open the kotlin_bridge header by command clicking in XCode.

We see a large header, but search for KotlinBridgeModule

Okay, we have our functions present here, but the name looks weird.
Kotlin_bridgeKotlinBridgeModule. WTF ?

Like React Native does, Kotlin Native also adds prefixes and suffixes in order to increase probability of uniqueness of names, as Objective C does not have package level visibility modifiers.

The functions have already converted Kotlin datatypes to Objective C datatypes. This is done by the kotlin compiler when we build for iOS.

Let’s try to access this in AppDelegate

Add this in your applicationDidFinish method.

  NSString *result = [[Kotlin_bridgeKotlinBridgeModule shared] syncBlockingFunction ];
NSLog(@"Result: %@", result);

Your code should look like this

Hit build. Go to console to check if this prints anything.

We see that the result is printed.

Very nice

To access the code at this stage

git checkout kotlin-invokefromobjc

But also useless. The kotlin module works but does nothing.

Okay, first lets do something about the name. The name is unusable by a dev, and the compiler will be upset seeing Kotlin_bridgeKotlinBridgeModule.

Go to your KotlinBridgeModule.kt file in Android Studio and add these annotations in front of the class and hit build.

@OptIn(ExperimentalObjCName::class)
@ObjCName("KotlinBridgeModule", exact = true)

Ideally it should always reflect when you do gradle build, but in my case I needed to remove Pods and reinstall. Sometimes build issues crop up, but nothing that can’t be solved by jugaad.

Hit build in Xcode and you should face an error.

Nice, this time it seems the old name is not recognised.

So does it mean that it has got updated ? Let’s check.
Open the kotlin header again by command clicking it in XCode.

Yes, the name has changed. Much better now.

Now let’s fix our code

NSString *result = [[KotlinBridgeModule shared] syncBlockingFunction ];
NSLog(@"Result: %@", result);

Now this seems fine. But what is this shared property ?
As you know, Object classes in Kotlin refer to Singleton classes. It is syntactic sugar for writing Safe Singletons, but nothing magical.

To access an Kotlin Object class in Java, you need to write

String syncResult = KotlinBridgeModule.INSTANCE.syncBlockingFunction

The INSTANCE property contains the singleton object. Similarly the shared property in Objective C holds the singleton object.

Accessing React Native in Kotlin Native

Now we start the interesting part. We have come a long way to reach here.
How do we do this ? How do we access React Native from Kotlin Native ?

To understand this, we need one last detour. Promise, just one small detour.

How does Kotlin Native access Native APIs ?

The Kotlin/Native compiler compiles Kotlin to LLVM IR code. LLVM is a compiler project to help with building compilers for multiple languages.

Every compiler that converts high level code to machine code needs to generate binaries for every processor architecture. We have lots of languages and lots of processor architectures. Writing compilers for every combination of them is an extraordinary task.

In order to help with writing compilers, the LLVM project provides lots of common tooling that are used by different compilers. It also provides an intermediate representation like bytecode called LLVM IR.

https://medium.com/media/3ab87ba3c35c06ae13898fc246397665/href

It is a complete domain, and to understand it you need to understand compiler engineering, but completely understanding it is not important for us right now. (maybe a future article ? let me know)

What’s highly relevant for us is, LLVM IR is like JVM bytecode in the sense that if you can generate LLVM IR for a language, at the IR level all languages have a common medium.
Other languages that also generate LLVM IR are C, C++, Objective C, Swift, etc.

Kotlin targeted Android and Java because it could be compiled to JVM Bytecode. Similarly Kotlin/Native targets C and Objective-C. Direct swift interop is work in progress, and C++ interop is not promised.

But we can use C++ and Swift with Kotlin through C and Objective-C.

Interfacing with React Native

React Native is primarily C++ with some Objective-C and JNI C++. If we write a pure Objective-C or pure C header, but write the implementations in C++ / Swift, then we can make Kotlin talk to C++ and Swift.

You do need to understand header files and implementation files in depth to understand this.

Header files declare the interface, Implementation files actually have the functionality.
You can write your code based on interfaces without bothering about implementation.
Only when your code is loaded into memory, or packaged into static binaries is when you need the implementation to be present.

This is exactly what we will exploit. React Native implementations are present in our iOS app, but we can write a library in Kotlin with just the interfaces. For that we use cinterop.

cinterop

The way to interop with C/Objective-C is provided through a mechanism called cinterop.

cinterop is a highly complex topic and needs a lot of time and skills to deal with. But doing simple things is not very hard.

Cinterop generates kotlin external declarations (like JNI) for your input source files. If the implementation is present at link-time, or if you also provide the implementation in static libraries or frameworks, then Kotlin can call those methods.

In our case, React Headers are available to us in node_modules. We don’t need to add all the headers too, only the ones relevant to us.
We also don’t need to add the entire contents of the headers, only those which we use.

Now, a natural question would be, won’t that create conflicts ? Won’t there be two copies ?

No, it won’t. If you have 2 implementations of the same interface with the same name, then there will be issues. But if you have the exact same interface, it will be like a forward declaration and not a redeclaration.

Why not directly include the headers ?

React Native uses macros and C++ heavily and if we naively import, cinterop will break. cinterop cannot expose macros, C++ and swift. So we need to build a minimal interface in Objective-C to expose the React frameworks to Kotlin

KotlinReact.h

With that out of the way, lets write a file KotlinReact.h in kotlin-bridge/cpp folder.

Let’s go to our NormalBridgeModule files, and list all interfaces we need.

  1. RCTBridge
  2. RCTBridgeModule
  3. RCTBridgeMethod
  4. RCTMethodInfo
  5. RCTModuleMethod
  6. RCTRegisterModule (we will ignore this, reason below)
  7. RCTResponseSenderBlock
  8. RCTPromiseResolveBlock
  9. RCTPromiseRejectBlock

There will be one difference from the Objective-C module. That will be in the load method. We do not want automatic initialization because then passing dependencies before creation becomes tricky. Our objective-c class could access everything so this was not an issue.

We want to put all these interfaces in one file for simplicity and not deal with header import order in cinterop. And also because as you will see, there are not many lines of code.

We will start with the easiest interfaces first, then write RCTBridge at last.

There should be a file called android/kotlin-bridge/cpp/kotlin_bridge.def
def files are the standard declarative way of doing cinterop.
We will be using the minimal cinterop file, and adding the headers in build gradle instead.

The def file contains

language = Objective-C
compilerOpts = -framework Foundation

This instructs the Kotlin compiler to build bindings for Objective C and use Foundation framework. Ideally we could have done this in gradle, but it seems to be a hard requirement to have this file. (Jan 2024)

Expose React Native headers to Kotlin Native

We will go header by header, starting with one with zero dependencies, and moving on to headers which use already added dependencies.
It is not always possible to do this, and it may be infeasible to build mathematically ground up like this, but for React this approach works.

In a future article, I will explain how we can expose C++ libraries like FlatBuffers without building from ground up by creating a new interface.

Let’s begin for React.

Expose the simplest parts, Blocks

Add these lines to android/kotlin-bridge/cpp/KotlinReact.h

#import <Foundation/Foundation.h>

typedef void (^RCTResponseSenderBlock)(NSArray *response);
typedef void (^RCTResponseErrorBlock)(NSError *error);
typedef void (^RCTPromiseResolveBlock)(id result);
typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error);

Here we added typedefs for multiple block definitions.

Now let us set up cinterop.

In your build.gradle.kts file, replace the lines

    iosX64()
iosArm64()
iosSimulatorArm64()

with

    listOf(
// iosX64(),
// iosArm64(),
iosSimulatorArm64()
).forEach {
it.compilations.getByName("main").cinterops {
val react by creating {
packageName("dev.shibasis.kotlin.react")
defFile(file("cpp/kotlin_bridge.def"))
headers("cpp/KotlinReact.h")
}
}

}

Each of those functions returns a KotlinNativeTarget. We need to configure cinterop for every KotlinNativeTarget, hence the forEach.

Then inside, we have the most important part. The cinterop configuration.
We have a simple def file, we add it here.
Remember cinterop generates Kotlin bindings for C/Objective-C types. Those bindings need a packageName, we do that here.

Header Paths

Then we specify paths to headers. This is okay for single header projects but for most libraries you will have tons of headers.
You can also specify the path your headers are located using includeDirs.
But it means that any headers in that directory needs to be C/Objective-C.

In a future article, we will cover how we can use CMakeLists and includeDirs to add any C++ library to Kotlin/Native.
We won’t worry about multi header libraries for this article.

Also add this to sourceSets block

    sourceSets {
val iosMain by creating
val iosX64Main by getting {
dependsOn(iosMain)
}
val iosArm64Main by getting {
dependsOn(iosMain)
}
val iosSimulatorArm64Main by getting {
dependsOn(iosMain)
}
}

This is needed to establish sources for ios apart from the common code. iosX64 means Intel Mac Simulator, iosArm64 is for iPhones and iosSimulatorArm64 means M1 Simulator. You can disable few of them as per your architecture to increase build speed.

Add these to gradle.properties

kotlin.mpp.enableCInteropCommonization=true
kotlin.native.cacheKind=none

Run gradle sync. Then run the commonizer gradle task

Create a new file in iosMain,

iosMain/kotlin/dev/shibasis/kotlin/bridge/DarwinBridgeModule.kt

Put this code there

package dev.shibasis.kotlin.bridge

import dev.shibasis.kotlin.react.RCTPromiseResolveBlock
import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName

fun test(block: RCTPromiseResolveBlock) {

}

@OptIn(ExperimentalObjCName::class)
@ObjCName("DarwinBridgeModule", exact = true)
object DarwinBridgeModule {
fun syncBlockingFunction() = "syncBlockingFunction"
fun normalAsyncFunction(callback: (Any) -> Unit) {
callback("normalAsyncFunction")
}
fun promiseFunction(resolve: (Any) -> Unit, reject: (Any) -> Unit) {
resolve("promiseFunction")
// reject("promiseFunctionError")
}
}

Now try to write any of the blocks to see if autocomplete is working.

This should not have any errors. Command click on the definition.

We got the bindings now.
Let’s try to check working.

Replace the callback in your DarwinBridgeModule from

fun normalAsyncFunction(callback: (Any) -> Unit) {
callback("normalAsyncFunction")
}

to

fun normalAsyncFunction(callback: RCTResponseSenderBlock) {
if (callback != null)
callback(listOf("normalAsyncFunction"))
}

Apart from changing the type we need to null check as Kotlin differentiates between nullable and non-null types.

We also need to send a list instead of a value because that is what RCTResponseSenderBlock requires.

typedef void (^RCTResponseSenderBlock)(NSArray *response);

Nice. Let’s check if this works.

Add this to your applicationDidFinish method

  RCTResponseSenderBlock callback = ^(NSArray *response) {
// Handle the response array
NSLog(@"Callback from Kotlin using RCTResponseSenderBlock : %@", response);
};

[[DarwinBridgeModule shared] normalAsyncFunctionCallback:callback];

Your code should look like this now,

Lets check the logs

Excellent !!!
We have exposed 7, 8 and 9 line items

Similarly change the promiseFunction from

Replace it with this function

fun promiseFunction(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
resolve?.invoke("promiseFunction")
// reject("promiseFunctionError")
}

Your code should now look like this

As these blocks are very similar in essence, these should work fine. Moving on.

Exporting RCTMethodInfo

These only rely on previous dependencies and hence are next.
Add these lines after the blocks

typedef struct RCTMethodInfo {
const char *const jsName;
const char *const objcName;
const BOOL isSync;
} RCTMethodInfo;

Your code should look like this now,

Let’s run the commonizer again.

Okay, this seems different from a data class. Now how do we use this ?
We refer to the official documentation

Mapping struct and union types from C - tutorial | Kotlin

Okay to build a object of a given struct, we use cValue
We convert the first declaration

const char *syncBlockingSelector = "syncBlockingFunction";
RCTMethodInfo *syncMethodInfo = new RCTMethodInfo{.objcName = syncBlockingSelector, .isSync = YES};

Kotlin ->

val methodInfo = cValue<RCTMethodInfo> {
isSync = true
objcName = "syncBlockingFunction"
}

But we get red lines. Why ?

Why ?
Okay, while Kotlin/Native converts Strings to const char * when Strings are directly used, it is unable to do the same with Strings inside structs.

We recheck the binding.

var objcName: kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVar>?

This isn’t a String, rather something very weird.
What is it ? And why not just a String ?

Constant Memory, Stack Memory, Heap Memory

We need to understand why const char *selector = “syncBlockingFunction” works in the first place.
Strings in C and C++ (and many other languages) are stored in the constants section of a compiled binary.

When we declare const char *selector = “syncBlockingFunction”, syncBlockingFunction is stored in the constants section and the address does not change.
It is why we can move these pointers across functions without it getting deallocated due to the function leaving the stack.

To understand more about memory segments in object files and processes, read this

Data segment - Wikipedia

Now Kotlin cinterop converts const char* to Kotlin.String as a special pointer optimization, but in this case it was not able to narrow the type.
It treated the const char *const as a generic char pointer.

We will have to look into why it didn’t recognise, but later.
We can send pointers. Lets do that now.

val arena = Arena()
val methodInfo = cValue<RCTMethodInfo> {
isSync = true
objcName = "syncBlockingFunction".cstr.getPointer(arena)
}

What is Arena() and what is cstr.getPointer() ?

Arena allocation is an optimisation where a region of memory is used to allocate and deallocate variables. This is the native heap for kotlin native.

The advantages from raw heap memory are:
1. Depending on the Arena structure, memory can be contiguous and hence have better cache behaviour and less fragmentation.
2. We can deallocate all variables in one go.

This is useful for us, and also the default in kotlin for native interop.

Region-based memory management - Wikipedia

It uses a modern version of malloc called mimalloc to perform the heap allocations.

Passing Pointers

When we need to send our Kotlin Strings to C/Objective-C where a char * is expected, we need to send a pointer.
We need the pointer to point to memory that won’t be garbage collected or stack deallocated. In order to ensure our strings remain in memory, we have two options: Pinning and Heap memory.

Memory Pinning in Kotlin Native means that the memory address being used remains stable for the duration of the native call. This is useful when we are calling native methods.
But, in our case we need the RCTMethodInfo to persist for as long as the app is active. (We can build lazy init and removal later), hence Heap memory is needed.

Now we need to pass this to RCTModuleMethod. But to do that we need RCTBridgeMethod and RCTModuleMethod exposed first.

Exposing RCTBridgeMethod

Add these lines to the header

@class RCTBridge;

typedef NS_ENUM(NSInteger, RCTFunctionType) {
RCTFunctionTypeNormal,
RCTFunctionTypePromise,
RCTFunctionTypeSync,
};

static inline const char *RCTFunctionDescriptorFromType(RCTFunctionType type)
{
switch (type) {
case RCTFunctionTypeNormal:
return "async";
case RCTFunctionTypePromise:
return "promise";
case RCTFunctionTypeSync:
return "sync";
}
};

@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 is the source from the RCTBridgeModule file.
Strictly speaking, we don’t need this much for our purpose, we could just expose this.

@protocol RCTBridgeMethod <NSObject>
@end

But we are exposing the full protocol because we need the bridge parameter for future. We will use the bridge parameter in a future article (Part III or Part IV), where we write a JSI module using this project.

Your code should look like this now

Run cinterop commonizer gradle task again.

Now lets check the generated kotlin bindings. This will be little long.

For the enum, we see cinterop intelligently converted the typedefs and the static function into one single class. I wonder why it didn’t do the String optimisation inside the struct. (Need to deep dive into cinterop)

For the protocol,

By now, this protocol definition seems normal.

const char * became Kotlin.CPointer

RCTFunctionType we have already seen

Some annotations that kotlin native compiler will use.

Okay, lets move on.

Exposing RCTModuleMethod

Add these lines to the header

@interface RCTModuleMethod : NSObject <RCTBridgeMethod>

@property (nonatomic, readonly) Class moduleClass;
@property (nonatomic, readonly) SEL selector;

- (instancetype)initWithExportedMethod:(const RCTMethodInfo *)exportMethod
moduleClass:(Class)moduleClass NS_DESIGNATED_INITIALIZER;

@end

We don’t need the other lines

Lets run commonizer again. This time, as this is an interface with inheritance, it generates a large interface. I will show the most relevant parts.

We can see that it inherits our protocol and also contains the prize. initWithExportedMethod.

Exposing RCTBridgeModule

Now lets expose the main protocol. Again we should not copy the entire thing, and only the relevant portions.

Add these lines to your header (large)

/**
* A class that allows NativeModules to call methods on JavaScript modules registered
* as callable with React Native.
*/
@interface RCTCallableJSModules : NSObject

// Commented out to avoid adding more headers.
//- (void)setBridge:(RCTBridge *)bridge;
//- (void)setBridgelessJSModuleMethodInvoker:(RCTBridgelessJSModuleMethodInvoker)bridgelessJSModuleMethodInvoker;

- (void)invokeModule:(NSString *)moduleName method:(NSString *)methodName withArgs:(NSArray *)args;
- (void)invokeModule:(NSString *)moduleName
method:(NSString *)methodName
withArgs:(NSArray *)args
onComplete:(dispatch_block_t)onComplete;
@end

@protocol RCTBridgeModule <NSObject>


// Implemented by RCT_EXPORT_MODULE
+ (NSString *)moduleName;

@optional

/**
* A reference to an RCTCallableJSModules. Useful for modules that need to
* call into methods on JavaScript modules registered as callable with
* React Native.
*
* To implement this in your module, just add `@synthesize callableJSModules =
* _callableJSModules;`. If using Swift, add `@objc var callableJSModules:
* RCTCallableJSModules!` to your module.
*/
@property (nonatomic, weak, readwrite) RCTCallableJSModules *callableJSModules;

/**
* A reference to the RCTBridge. Useful for modules that require access
* to bridge features, such as sending events or making JS calls. This
* will be set automatically by the bridge when it initializes the module.
* To implement this in your module, just add `@synthesize bridge = _bridge;`
* If using Swift, add `@objc var bridge: RCTBridge!` to your module.
*/
@property (nonatomic, weak, readonly) RCTBridge *bridge;

/**
* The queue that will be used to call all exported methods. If omitted, this
* will call on a default background queue, which is avoids blocking the main
* thread.
*
* If the methods in your module need to interact with UIKit methods, they will
* probably need to call those on the main thread, as most of UIKit is main-
* thread-only. You can tell React Native to call your module methods on the
* main thread by returning a reference to the main queue, like this:
*
* - (dispatch_queue_t)methodQueue
* {
* return dispatch_get_main_queue();
* }
*
* If you don't want to specify the queue yourself, but you need to use it
* inside your class (e.g. if you have internal methods that need to dispatch
* onto that queue), you can just add `@synthesize methodQueue = _methodQueue;`
* and the bridge will populate the methodQueue property for you automatically
* when it initializes the module.
*/
@property (nonatomic, readonly) dispatch_queue_t methodQueue;


/**
* Most modules can be used from any thread. All of the modules exported non-sync method will be called on its
* methodQueue, and the module will be constructed lazily when its first invoked. Some modules have main need to access
* information that's main queue only (e.g. most UIKit classes). Since we don't want to dispatch synchronously to the
* main thread to this safely, we construct these modules and export their constants ahead-of-time.
*
* Note that when set to false, the module constructor will be called from any thread.
*
* This requirement is currently inferred by checking if the module has a custom initializer or if there's exported
* constants. In the future, we'll stop automatically inferring this and instead only rely on this method.
*/
+ (BOOL)requiresMainQueueSetup;

/**
* Injects methods into JS. Entries in this array are used in addition to any
* methods defined using the macros above. This method is called only once,
* before registration.
*/
- (NSArray<id<RCTBridgeMethod>> *)methodsToExport;

/**
* Injects constants into JS. These constants are made accessible via NativeModules.ModuleName.X. It is only called once
* for the lifetime of the bridge, so it is not suitable for returning dynamic values, but may be used for long-lived
* values such as session keys, that are regenerated only as part of a reload of the entire React application.
*
* If you implement this method and do not implement `requiresMainQueueSetup`, you will trigger deprecated logic
* that eagerly initializes your module on bridge startup. In the future, this behaviour will be changed to default
* to initializing lazily, and even modules with constants will be initialized lazily.
*/
- (NSDictionary *)constantsToExport;

/**
* Notifies the module that a batch of JS method invocations has just completed.
*/
- (void)batchDidComplete;

/**
* Notifies the module that the active batch of JS method invocations has been
* partially flushed.
*
* This occurs before -batchDidComplete, and more frequently.
*/
- (void)partialBatchDidFlush;

@end

Run the commonizer again.

We get the protocol in kotlin.

Until now we had used RCTBridge as a forward declaration, an opaque class.
Now we will refine it to contain few more fields. This is optional, we are doing this to extract the jsi::Runtime for next chapter where we will be writing JSI Modules in Kotlin/Native.

Exposing RCTBridge and RCTCxxBridge

@interface RCTBridge
@end

@interface RCTCxxBridge : RCTBridge
@property (nonatomic, readonly) void *runtime;
@end

Rerun the commonizer

We get this, all ready for JSI (upcoming chapter)

Rewriting DarwinBridgeModule

We had written dev code in our test function, but that won’t do.
Let’s be good engineers and write a proper module.

Inherit RCTBridgeModule

Make DarwinBridgeModule extend RCTBridgeModuleProtocol

We get red lines. Why ?

Many classes in Objective-C inherit from NSObject and get its functionalities. We will also need to inherit NSObject.

Nice, red lines are gone.

Creating the static methods

There were two class methods in our module

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

Now we will implement moduleName in this class. We don’t want to implement load due to 2 reasons.

  1. load as a method is not available in Kotlin. init is available as a replacement.
  2. load automagically registers your module on importing the header. When we want to create the kotlin module on objective c, we want to retain some control to pass dependencies, etc.

Create the Arena on init

Move the arena to your object class as a private field.

Write a function close(), which clears the arena.

Now override the method we need. To recap, we had overriden methodsToExport in Objective C like this

So naturally we will be overriding the same method here.

It shows up as List<*> because there was “id” in the definition. But don’t be tricked, we still need to send RCTBridgeMethods (or RCTModuleMethods).
Just import and rename the type to List<RCTBridgeMethodProtocol>. It does not make a difference to the Kotlin compiler, but it will prevent programmer errors.

Your code should be like this now,

override fun methodsToExport(): List<RCTBridgeMethodProtocol> {

}

SyncBlockingFunction

Objective-C

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

Kotlin

val syncMethodInfo = cValue<RCTMethodInfo> {
isSync = true
objcName = "syncBlockingFunction".cstr.getPointer(arena)
}
val syncMethod = RCTModuleMethod.alloc()?.initWithExportedMethod(syncMethodInfo, this::`class`)

While this is the direct translation, kotlin cinterop goes one step ahead, converts this into a constructor.

So we should write,

val syncMethodInfo = cValue<RCTMethodInfo> {
isSync = true
objcName = "syncBlockingFunction".cstr.getPointer(arena)
}
val syncMethod = RCTModuleMethod(syncMethodInfo, this.`class`())

This looks like

Red lines again. Why ?

It needs a pointer, but we are sending a value. Okay.
Kotlin documentation suggests to convert into arena allocated pointers for these issues.

Mapping struct and union types from C - tutorial | Kotlin

We solved this earlier by allocating in the Arena. Let’s do it again.

val syncMethodInfo = arena.alloc<RCTMethodInfo>()
syncMethodInfo.isSync = true
syncMethodInfo.objcName = "syncBlockingFunction".cstr.getPointer(arena)
val syncMethod = RCTModuleMethod(syncMethodInfo.ptr, this.`class`())

Arena.alloc<Type> causes an arena allocation for your type and returns you data.
The “ptr” extension property returns you the pointer pointing to your data.

Your code should now look like this

Phew. Of course it needs to work in order for us to celebrate.
Let’s quickly write the bindings for the other two functions.

NormalAsyncFunction

Objective-C

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

Kotlin

val asyncMethodInfo = arena.alloc<RCTMethodInfo>()
asyncMethodInfo.isSync = false
asyncMethodInfo.objcName = "normalAsyncFunction:(RCTResponseSenderBlock)callback".cstr.getPointer(arena)
val asyncMethod = RCTModuleMethod(asyncMethodInfo.ptr, this.`class`())

PromiseFunction

Objective-C

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]];

Kotlin

val promiseMethodInfo = arena.alloc<RCTMethodInfo>()
promiseMethodInfo.isSync = false
promiseMethodInfo.objcName = "promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject".cstr.getPointer(arena)
val promiseMethod = RCTModuleMethod(promiseMethodInfo.ptr, this.`class`())Return the list
return listOf(
syncMethod,
asyncMethod,
promiseMethod
)

Your code should look like this now,

The entire module code.
I have prefixed the strings we return with “Kotlin: “ to better identify in logs.

https://medium.com/media/ca1e13d7d671239ee1efa7664ea36987/href

Alternative to RegisterModule ?

We needed some control over initialization as we may need to pass dependencies in the future. Kotlin cannot freely import headers similar to how a co-located objective-c implementation could, so dependency management is critical.

Thankfully React Native provides an extraModulesForBridge method in the RCTAppDelegate (RCTBridgeDelegate). It returns a NSArray<RCTBridgeModule>, so we can just put our module in a list and return it.

Let’s override it and return then.

- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
return @[[DarwinBridgeModule new]];
}

Alas, we are getting another error. What is this ?

Kotlin subclass of Objective-C can’t be imported. What. Is this real ?

Unfortunately, Yes.
Kotlin subclasses of Objective-C are not imported.

Give up ? Persist ?

But we do know that this is an elementary usecase. Half of the Kotlin/Native functionality shouldn’t work then.
There must be a workaround. There must be some example in the existing ecosystem.

Ray of Hope, Compose Container

Jetpack Compose is the UI framework for Kotlin Multiplatform. We won’t go into that for this article, but there is something highly relevant for us there.

ComposeContainer. ComposeContainer is a subclass of UIViewController. UIViewController is an Objective-C class. And without ComposeContainer there is no Compose on iOS.

But clearly, there is Compose on iOS, and it works fine.

Limited Support for Objective-C classes

Reading the source code for compose(iOS) gives us actionable insight.
We know that Kotlin subclasses aren’t supported, and you cannot have complex inheritance heirarchies.

But you can send instances of those classes. You can send objects.

We need to create a simple factory method to just create our instance and send it. We can also pass dependencies if they are needed.

Factory Method for Kotlin subclasses of Objective-C classes

Let’s write the simplest factory method.

fun getModule(): RCTBridgeModuleProtocol = DarwinBridgeModule()

To use this, in your Objective-C code

- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
return @[[Kotlin_bridgeDarwinBridgeModuleKt getModule]];
}

Kotlin has package scoped free functions, but when you wish to use them in Objective-C the filename gets transformed into an Objective-C class and your free functions become class methods.

Nothing extraordinary. Moving on.

Build Successful, Runtime Error

It is unable to invoke syncBlockingFunction. But the mere fact that syncBlockingFunction is present in the error means our methodsToExport has partially worked.

But why is this happening ? Let’s print all methods of the class.

Write this function in your Objective-C file. This function uses the objective c runtime API to iterate over the methods present and print the selectors for those methods (courtesy of GPT).

void listMethodsForClass(Class cls) {
unsigned int methodCount = 0;

Method *methods = class_copyMethodList(cls, &methodCount);

for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
const char *name = sel_getName(selector);
NSLog(@"%s:Method: %s", class_getName(cls), name);
}

free(methods);
}

Let’s print the object we created.

Oh. Our methods syncBlockingFunction, normal and promise are not printed here. But methodsToExport is.
(The other methods are from the Kotlin/Native runtime. You may be surprised to know that Kotlin/Native is also implemented in C++. One day, we will deep dive there too, but currently compilers are out of my skill area.)

Why is methodsToExport present ? And again going back to compose, why does that ViewController work ?

Hypothesis: If the Objective-C interface has those methods, they are exposed.

I believe it has to do with the method signatures present in the Objective-C interface.
methodsToExport is present in RCTBridgeModule and hence it is available in the generated objects. It is the same with ViewController. All lifecycle methods and other methods are present in the Objective-C interface.

Our functions are not. It is a hypothesis (it works in this case) and in order to prove it for all cases, I need to study the Kotlin/Native runtime source and I will write how it works.

But now we will test out our hypothesis.

Add this interface to KotlinReact.h, the bindings file where we declared our React interfaces.

@protocol BridgeModule <NSObject, RCTBridgeModule>
- (NSString *)syncBlockingFunction;
- (void)normalAsyncFunction:(RCTResponseSenderBlock)callback;
- (void)promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
@end

Now again run cinterop, using the gradle tool window

cinterop works with few conventions. For any protocol you define in the binding, it will generate a Kotlin interface suffixed with Protocol.
We wrote @protocol BridgeModule. Hence it will generate BridgeModuleProtocol.

Let’s change our superclass from RCTBridgeModuleProtocol to BridgeModuleProtocol

We don’t need to change our getModule method as Kotlin cinterop carries the hierarchy from native bindings.
Our objective-c protocol had a super class of RCTBridgeModule, hence the Kotlin interface also has the same super class.

Let’s see the generated Kotlin interface

Beautiful, isn’t it. The debugDescription method is for kotlin/native internals, we don’t need to worry about it.

Let’s see our Objective-C protocol again so we can mentally map them properly.

@protocol BridgeModule <NSObject, RCTBridgeModule>
- (NSString *)syncBlockingFunction;
- (void)normalAsyncFunction:(RCTResponseSenderBlock)callback;
- (void)promiseFunction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
@end

Okay our 3 functions match in their return types, arguments and names. Not surpising anymore. Let’s proceed.

We should also be getting errors in the file.

We have these functions in our protocol, so we need to add the override keyword.

Let’s see the entire code once before we build.

https://medium.com/media/8f6e505a99359081dc167fd1c384506e/href

I have made some non-conventional formatting to make it fit in one shot.

Done. Now for the moment of truth.
Let’s return to XCode and hit build.

Hurray !!! We built a pure Kotlin Bridge module and learnt a lot of things across the way.

If you made this far, you should be proud. It was quite difficult, but we gradually broke it down and got some understanding about how these complex systems work.

The final code is present in GitHub in the kotlin-invokefromobjc branch

git clone https://github.com/shibasis0801/KotlinBridgeModule.git
git checkout kotlin-invokefromobjc

I hope this helps and inspires you to dive deep.

The next article, we will expose FlexBuffers (C++) to Kotlin Multiplatform using both JNI, cinterop and expect/actual.

Thank you for reading.
Shibasis.