All posts

Bridging in React Native

An in-depth look into React Native's core

On this post I assume you know the basics of React Native, and will focus on how the internals work when managing the communication between native and JavaScript.

Main Threads

Before anything else, keep in mind that there are 3 “main” threads* in React Native:

  • The shadow queue: where the layout happens
  • The main thread: where UIKit does its thing
  • The JavaScript thread: where your JS code is actually running

Plus every native module has its own GCD Queue unless it specifies otherwise (a more detailed explanation is coming).

* The “shadow queue” is actually a GCD Queue rather than a thread, as the name suggests.

Native Modules

If you don’t know how to create a Native Module yet, I’d recommend you check the documentation before.

Here’s an example Person native module, that both, receives calls from JavaScript and calls into JS.

@interface Person : NSObject <RCTBridgeModule>
@end

@implementation Logger

RCT_EXPORT_MODULE()

RCT_EXPORT_METHOD(greet:(NSString *)name)
{
  NSLog(@"Hi, %@!", name);
  [_bridge.eventDispatcher sendAppEventWithName:@"greeted"
                                           body:@{ @"name": name }];
}

@end

We are going to focus on these two macros, RCT_EXPORT_MODULE and RCT_EXPORT_METHOD, what they expand into, what are their roles and how does it work from there.

RCT_EXPORT_MODULE([js_name])

As the name suggests, it exports your modules, but what does export mean in this specific context? It means making the bridge aware of your module.

Its definition is actually pretty simple:

#define RCT_EXPORT_MODULE(js_name) \
  RCT_EXTERN void RCTRegisterModule(Class); \
  + (NSString \*)moduleName { return @#js_name; } \
  + (void)load { RCTRegisterModule(self); }

What does it do:

  • It first declares RCTRegisterModule as an extern function, which means that the implementation of the function is not visible to the compiler but will be available at link time, than
  • declares a method moduleName, that returns the optional macro parameter js_name, in case you want your module to have a name in JS other than the Objective-C class name, and last
  • declares a load method (When the app is loaded into memory it’ll call the load method for every class) that calls the above declared RCTRegisterModule function to actually make the bridge aware of this module.

RCT_EXPORT_METHOD(method)

This macro is “more interesting”, it doesn’t add anything to your actual method, it actually creates a new method in addition to declaring the one specified.

This new method would look something like that for our example:

+ (NSArray *)__rct_export__120
{
  return @[ @"", @"log:(NSString *)message" ];
}
“What the heck is that?” would be a very acceptable first reaction.

It’s generated by concatenating the prefix (__rct_export__) with an optional js_name (empty in this case) with the line number of the declartion (e.g. 12) with the __COUNTER__ macro.

The purpose of this method is only returning an array that contains the optional js_name (again, empty in this case) and the method signature. The hack on the name is only to avoid method clashing*.

*it’s still technically possible to have 2 generated methods with the same name if you’re using a category, but very much unlikely and shouldn’t result in any expected behaviour, although Xcode will warn you that it has an unexpected behaviour.

Runtime

This whole setup is only to provide information to the bridge, so it can find everything that was exported, modules and methods, but this all happens at load time, now we’ll look at how it’s used at runtime.

Here’s the bridge initialisation dependency graph:

Bridge Initialisation

Initialise Modules

All the RCTRegisterModule function does is add the class to an array so the bridge can find it later when a new bridge instance is created. It goes through the modules array, create an instance of every module, store a reference to it on the bridge, give it a reference back to the bridge (so we can call both ways), and check if it has specified in which queue it wants to run, otherwise we give it a new queue, separate from all other modules.

NSMutableDictionary *modulesByName; // = ...
for (Class moduleClass in RCTGetModuleClasses()) {
  // ...
  module = [moduleClass new];
  if ([module respondsToSelector:@selector(setBridge:)]) {
    module.bridge = self;
  }
  modulesByName[moduleName] = module;
  // ...
}

Configure Modules

Once we have our modules, in a background thread, we list all the methods for each module, and call the methods that begin with __rct_export__, so we can have a string representation of the method signature. That’s important so we can have the actual types of the parameters, i.e. at runtime we’d only be able to know that a parameter is an id, this way we can know that it’s actually an NSString * in this case.

unsigned int methodCount;
Method *methods = class_copyMethodList(moduleClass, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
  Method method = methods[i];
  SEL selector = method_getName(method);
  if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
    IMP imp = method_getImplementation(method);
    NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector);
    //...
    [moduleMethods addObject:/* Object representing the method */];
  }
}

Setup JavaScript Executor

The JavaScript executors have a -setUp method that allows it to do more expensive work, like initialising JavaScriptCore, on a background thread. It also saves some work, since only the active executor will receive the setUp call, rather than all the executors available.

JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
_context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];

Inject JSON Configuration

The JSON configuration containing only our module would be something like:

{
  "remoteModuleConfig": {
    "Logger": {
      "constants": { /* If we had exported constants... */ },
      "moduleID": 1,
      "methods": {
        "requestPermissions": {
          "type": "remote",
          "methodID": 1
        }
      }
    }
  }
}

This is store in the JavaScript VM as a global variable, so when the JS side of the bridge is initialised it can use this information to create the modules.

Load JavaScript Code

This is pretty intuitive, just load the source code from whatever provider was specified. Usually downloads the source from the packager during development or load it from disk in production.

Execute JavaScript Code

Once everything is ready, we can load the application source code in the JavaScriptCore VM, that will copy the source, parse and execute it. In the initial execution it should register all the CommonJS modules and require the entry point file.

JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError);
JSStringRelease(jsURL);
JSStringRelease(execJSString);

Modules in JavaScript

The modules generated from JSON configuration showed above will be available in JavaScript through the NativeModules object of react-native, e.g.:

var { NativeModules } = require('react-native');
var { Person } = NativeModules;

Person.greet('Tadeu');

The way it works is that when you call a method it goes to a queue, containing the module name, the method name and all the arguments used to call it. At the end of the JavaScript execution this queue is given back to native to execute this calls.

Call cycle

Now if we call the module with the code from above, here’s what it looks like:

Graph

The calls have to start from native* it calls into JS, and during the execution, as it calls methods on NativeModules, it enqueues calls that have to be executed on the native side. When the JS finishes, native goes through the enqueued calls, and as it executes them, callbacks and calls through the bridge (using the _bridge instance available through a native module to call enqueueJSCall:args:) are used to call back into JS again.

* The graph just pictures a moment middle JavaScript execution NOTE: If you’ve been following the project, there used to be a queue of calls from native -> JS as well, that’d be dispatched on every vSYNC, but it’s been removed in order to improve start up time.

Argument types

For calls from native to JS it’s easier, the arguments are passed as an NSArray that we just encode as JSON, but for the calls from JS we need the native type, for that we check for primitives explicitly (i.e. ints, floats, chars, etc…) but as mentioned above, for any objects (and structs), the runtime doesn’t give us enough information from the NSMethodSignature, and we save the types as strings.

We use regular expression to extract the types from the method signature, and we use the RCTConvert utility class to actually transform the objects, it has a method for every type supported by default, and it tries to convert the JSON input into the desired type.

We use objc_msgSend to call the method dynamically, unless it’s a struct, since there’s no version of objc_msgSend_stret on arm64, so we fallback to NSInvocation.

Once we converted all the arguments, we use another NSInvocation to call the target module and method, with all the parameters.

Here’s an example:

// If you had the following method in a given module, e.g. `MyModule`
RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {}

// And called it from JS, like:
require('NativeModules').MyModule.method(['a', 1], {
  x: 0,
  y: 0,
  width: 200,
  height: 100
});

// The JS queue sent to native would then look like the following:
// ** Remember that it's a queue of calls, so all the fields are arrays **
@[
  @[ @0 ], // module IDs
  @[ @1 ], // method IDs
  @[       // arguments
    @[
      @[@"a", @1],
      @{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 }
    ]
  ]
];

// This would convert into the following calls (pseudo code)
NSInvocation call
call[args][0] = GetModuleForId(@0)
call[args][1] = GetMethodForId(@1)
call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1])
call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
call()

Threading

As mentioned above, every module will have it’s own GCD Queue by default, unless it specifies the queue it wants to run on, by implementing the -methodQueue method or synthesizing the methodQueue property with a valid queue. The exceptions are View Managers* (That extend RCTViewManager) that will use the Shadow Queue by default, and the special target RCTJSThread, which is just a placeholder, since it’s a thread rather than a queue.

* View Managers not a real exception, since the base class explicitly specifies the shadow queue as the target queue.

The current threading “rules” are as following:

  • -init and -setBridge: are guaranteed to be called on the main thread;
  • All the exported methods are guaranteed to be called on the target queue;
  • If you implement the RCTInvalidating protocol, invalidate is also guaranteed to be called on the target queue;
  • There’s no guarantees for which thread -dealloc is going to be called from.

When a batch of calls is received from JS, the calls will be groupped by target queue, and dispatched in parallel:

// group `calls` by `queue` in `buckets`
for (id queue in buckets) {
  dispatch_block_t block = ^{
    NSOrderedSet *calls = [buckets objectForKey:queue];
    for (NSNumber *indexObj in calls) {
      // Actually call
    }
  };

  if (queue == RCTJSThread) {
    [_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
  } else if (queue) {
    dispatch_async(queue, block);
  }
}

The end

That’s it, that’s a slightly more in-depth overview of how the bridge works. I hope that people that want to build more complex modules and/or contribute to the core framework find it helpful.

Feel free to reach out to me if anything wasn’t clear, or not in-depth enough or if there’s anything you’d like to hear about next!