skip to content

Swift & Objective-C++ Glue

On iOS a Turbo Module is an Objective-C++ class conforming to the generated spec protocol, often wrapping Swift through an adapter. Interviewers probe the .mm glue and main-queue work.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In React Native 0.87, what must an iOS Turbo Module's Objective-C++ class implement to satisfy its Codegen-generated spec?

level: middleimportance: must knowfreq 45%

answer

  1. adopt the <Name>Spec protocol
  2. selectors copied from the generated header
  3. getTurboModule returns the SpecJSI
  4. +moduleName matches the JS name
  5. modulesProvider maps name to class

basics

~20 s

The class adopts the generated NativeXxxSpec protocol, implements every selector it declares, returns the generated NativeXxxSpecJSI from getTurboModule:, returns the JavaScript name from +moduleName, and is registered in codegenConfig.ios so React Native can map that name to the class.

solid answer

~30 s

Codegen turns a spec like `NativeSmartLock.ts` into a protocol `NativeSmartLockSpec` (extending `RCTBridgeModule` and `RCTTurboModule`) and a C++ class `NativeSmartLockSpecJSI`. The `.mm` class must: adopt the protocol in its header by importing `<SmartLockSpec/SmartLockSpec.h>`; implement every declared selector exactly — a `Promise` method gains trailing `resolve:` and `reject:` blocks, a synchronous one returns its value; implement `getTurboModule:` returning `std::make_shared<facebook::react::NativeSmartLockSpecJSI>(params)`; and implement `+moduleName` returning the same name JavaScript asks `TurboModuleRegistry` for. Finally, list it under `codegenConfig.ios` in `package.json` (`modulesProvider` in the tutorial) and re-run `pod install`. No `RCT_EXPORT_MODULE` or `RCT_EXPORT_METHOD` is involved.

code

objective-cpp · 43 lines
objective-cpp
// RCTSmartLock.h
#import <Foundation/Foundation.h>
#import <SmartLockSpec/SmartLockSpec.h>

@interface RCTSmartLock : NSObject <NativeSmartLockSpec>
@end

// RCTSmartLock.mm
#import "RCTSmartLock.h"

@implementation RCTSmartLock

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

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
  return std::make_shared<facebook::react::NativeSmartLockSpecJSI>(params);
}

// sdkVersion(): string
- (NSString *)sdkVersion
{
  return @"2.4.0";
}

// unlock(lockId: string): Promise<void>
- (void)unlock:(NSString *)lockId
       resolve:(RCTPromiseResolveBlock)resolve
        reject:(RCTPromiseRejectBlock)reject
{
  if (lockId.length == 0) {
    reject(@"E_BAD_ID", @"lockId must not be empty", nil);
    return;
  }
  // ...call into the lock SDK here...
  resolve(nil);
}

@end

go deeper

for a junior

Recall the four pieces: adopt the generated protocol, implement its methods, write getTurboModule:, and return the module name, then register it in package.json.

for a middle

Derive the selectors from a spec (sync return, Promise with resolve and reject), explain what the SpecJSI class is for, and say where protocol and header names come from.

for a senior

Diagnose a module that compiles but cannot be found from JavaScript: name drift across +moduleName, the codegenConfig key and getEnforcing, or stale generated code.

for a principal

Treat the generated protocol as the cross-language contract and decide how a team keeps spec, native code and registration in step across releases, for example in review and CI.

## The contract in one sentence A **Turbo Module** on iOS is an Objective-C++ class that **adopts a protocol Codegen wrote from your TypeScript spec**, hands React Native a **generated C++ JSI object** for itself, and is **registered by name** in `package.json`. Everything else — which thread it uses, how it emits events, how it is packaged — layers on top of that contract. ## What Codegen gives you to implement For a spec file `NativeSmartLock.ts` in a project whose `codegenConfig.name` is `SmartLockSpec`, running `pod install` makes Codegen produce `SmartLockSpec/SmartLockSpec.h`. The pieces your class touches: - **`NativeSmartLockSpec`** — an Objective-C protocol that extends `RCTBridgeModule` and `RCTTurboModule`, with one selector per spec method. The protocol is named after the **spec file**; the header is named after **`codegenConfig.name`**. - **`facebook::react::NativeSmartLockSpecJSI`** — a C++ class deriving from `ObjCTurboModule`. JavaScript holds this object; it converts arguments and forwards each call to your instance. - **`JS::` structs** for typed object parameters, when the spec uses them. You can jump to the protocol from Xcode, and Xcode can generate method stubs from it. ## The five things the class must do 1. **Adopt the protocol** in its header: `@interface RCTSmartLock : NSObject <NativeSmartLockSpec>`, after importing `<SmartLockSpec/SmartLockSpec.h>`. 2. **Implement every protocol selector** with the generated signature. The mapping is mechanical: | Spec method | Generated selector shape | |---|---| | `sdkVersion(): string` | `- (NSString *)sdkVersion;` — returns a value synchronously | | `unlock(lockId: string): Promise<void>` | `- (void)unlock:(NSString *)lockId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;` | | `lock(lockId: string): void` | `- (void)lock:(NSString *)lockId;` — fire and forget | 3. **Implement `getTurboModule:`**, taking `const facebook::react::ObjCTurboModule::InitParams &` and returning `std::make_shared<facebook::react::NativeSmartLockSpecJSI>(params)`. This is the factory through which React Native obtains the JSI wrapper for your instance, and it is why the file must be `.mm`. 4. **Implement `+moduleName`**, returning the JavaScript-facing name. `RCTBridgeModule` requires it, and it should equal the string the spec passes to `TurboModuleRegistry.getEnforcing`, here `'NativeSmartLock'`. 5. **Be registered.** Add the name-to-class mapping under `codegenConfig.ios` — the 0.87 tutorial uses `"modulesProvider": { "NativeSmartLock": "RCTSmartLock" }`, and the Codegen reference documents the equivalent per-module form `ios.modules.NativeSmartLock.className` — then re-run `pod install`. Codegen writes an `RCTModuleProviders.mm` that maps each name to its class, and the app's `RCTAppDependencyProvider` hands that map to React Native. ## What is not part of the contract any more - **`RCT_EXPORT_MODULE`** — the legacy registration macro. The module is found through the Codegen-generated provider map instead. - **`RCT_EXPORT_METHOD`** — the legacy way to expose a method. The method list now comes from the protocol, which comes from the spec. - **`NativeModules.SmartLock`** on the JavaScript side — replaced by the typed spec's default export. Since React Native 0.82 the New Architecture is the only architecture, so this protocol-plus-factory shape is the way to write an iOS module. Legacy modules still load through the interop layer, but they are not what a new module should look like. ## Where the pieces live on disk - **Your code:** `RCTSmartLock.h` and `RCTSmartLock.mm`, in an app group or a library's `ios/` folder. - **Generated code:** the `SmartLockSpec` header and its `-generated.mm` companion, rewritten by Codegen on every `pod install`; never edit them by hand, because the next install overwrites them. - **Registration:** `package.json`'s `codegenConfig`, read by Codegen at the same time. In an Expo project the same files appear after prebuild; the Expo Modules API is a different authoring route with its own contract. ## What goes wrong when a piece is missing - **Selector mismatch** — a hand-written signature that differs from the protocol (a renamed argument label, a missing `reject:`) produces a compiler warning about an unimplemented protocol method, and a call from JavaScript has no method to land on. - **Name mismatch** — if `+moduleName`, the `modulesProvider` key and the name in `getEnforcing` disagree, JavaScript throws `TurboModuleRegistry.getEnforcing(...): 'NativeSmartLock' could not be found`. - **Stale generated code** — editing the spec without re-running `pod install` leaves the old protocol in place, so new methods do not exist on the native side. - **Returning `nullptr` from `getTurboModule:`** — React Native logs an error and the module is unusable. ## How interviewers probe it They usually hand you a spec with one synchronous getter and one `Promise` method and ask for the `.mm`. A strong answer names the protocol, derives both selectors correctly, writes the factory, mentions `+moduleName` and registration, and says that `pod install` regenerates the header. Spec authoring, the threading of each call kind and event emitters are separate topics.

  • Why does the protocol name differ from the generated header's name?
    They come from different inputs. The protocol and the `...SpecJSI` class are named after the spec file (`NativeSmartLock.ts` gives `NativeSmartLockSpec`), while the header and its folder are named after `codegenConfig.name` (`SmartLockSpec/SmartLockSpec.h`). One library can hold several specs, so several protocols share one header.
  • What does React Native do with the name returned by +moduleName?
    It is the identity the module answers to. `RCTBridgeModule` requires the method, and it should match both the key under `codegenConfig.ios` and the string the spec passes to `TurboModuleRegistry.getEnforcing`. When they drift apart, the lookup from JavaScript fails with a could-not-be-found error even though the class compiles.
  • How do you find the exact selectors to implement?
    Open the generated header after `pod install` — Xcode can jump to the `NativeSmartLockSpec` protocol and generate stubs. Do not guess: a `Promise` method gains `resolve:` and `reject:` labels, and numbers arrive as `double`, so a hand-written signature easily misses the protocol.

saying these in an interview costs you the question

  • You still need RCT_EXPORT_METHOD on each method so JavaScript can see it
  • The protocol and the header are both named after codegenConfig.name
  • getTurboModule: can return self because the class already adopts the spec
  • Codegen output updates by itself when you edit the spec, no pod install needed
  • +moduleName is optional once the class is listed in modulesProvider
open as a page

In React Native on iOS, why must a Turbo Module's implementation file be Objective-C++ (.mm) rather than plain Objective-C (.m)?

level: juniorimportance: should knowfreq 30%

basics

~20 s

The Codegen spec is partly C++: getTurboModule: must return a std::shared_ptr to a generated C++ JSI class, and typed object parameters arrive as C++ structs. Only an Objective-C++ (.mm) file compiles C++ and Objective-C together.

open as a page

When migrating a legacy React Native iOS module built on RCT_EXPORT_MODULE and RCT_EXPORT_METHOD to a Turbo Module, what replaces each macro?

level: middleimportance: should knowfreq 35%

basics

~20 s

RCT_EXPORT_MODULE's two jobs split: you write +moduleName yourself and register the class under codegenConfig.ios in package.json. RCT_EXPORT_METHOD disappears: the TypeScript spec declares each method and the class implements the matching selector from the generated protocol.

open as a page

In a React Native iOS Turbo Module, how should UIKit work reach the main thread, and what do requiresMainQueueSetup and methodQueue change?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Asynchronous Turbo Module methods arrive on a background method queue, so UIKit calls must be dispatched to the main queue inside the method. +requiresMainQueueSetup only decides where the module is constructed; overriding methodQueue to force every call onto main is deprecated.

open as a page

In React Native on iOS, how do you wrap a vendor's Swift-only smart-lock SDK as a Turbo Module, and how do you split the work between Swift and Objective-C++?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use an adapter: a Swift class that calls the vendor SDK and exposes an Objective-C-compatible surface, plus a thin Objective-C++ class that adopts the Codegen spec, owns the adapter and forwards each call, because Swift cannot implement the C++-bearing spec directly.

open as a page