nitro icon indicating copy to clipboard operation
nitro copied to clipboard

Complex callback causes iOS to not build

Open ronickg opened this issue 2 months ago • 4 comments

What's happening?

I have a bit of a more complicated callback structure and useage of hybrid objects. The code works completely fine on android, but on iOS it doesn't compile. Provided a sample repo to test with.

I am not sure if my swift code is actually just wrong (Barely know swift), but I don't think so.

Reproduceable Code

Repo: https://github.com/ronickg/bug


import type { HybridObject } from 'react-native-nitro-modules';

export interface ComplexData {
  url: string;
  statusCode: number;
  headers: Record<string, string>;
  metadata: string[];
}
export interface MyCallback {
  onSimpleEvent(message: string): void;
  onComplexEvent(data: ComplexData): void;
  onDataReceived(data: ComplexData, buffer: ArrayBuffer): void;
  onMaybeData(data: ComplexData | undefined): void;
  onMultiParam(
    data: ComplexData,
    buffer: ArrayBuffer,
    optional: ComplexData | undefined
  ): void;
}

export interface CallbackTester
  extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
  createBuilder(callback: MyCallback): CallbackBuilder;
}

export interface CallbackBuilder
  extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
  setMessage(msg: string): void;
  trigger(): void;
}

Swift Code:

import Foundation
import NitroModules
class HybridCallbackTester: HybridCallbackTesterSpec {
  func createBuilder(callback: MyCallback) throws -> any HybridCallbackBuilderSpec {
    return HybridCallbackBuilder(callback: callback)
  }
}

class HybridCallbackBuilder: HybridCallbackBuilderSpec {
  private let callback: MyCallback

  init(callback: MyCallback) {
    self.callback = callback 
  }

  private var message: String = "default"

  func setMessage(msg: String) throws {
    self.message = msg
  }

  func trigger() throws {
    // Create complex test data
    let complexData = ComplexData(
      url: "https://example.com/test",
      statusCode: 200,
      headers: ["Content-Type": "application/json", "X-Custom": "value"],
      metadata: ["meta1", "meta2", "meta3"]
    )

    // Create test ArrayBuffer
    let testString = "Test data here!"
    let buffer = ArrayBuffer.allocate(size: testString.utf8.count)
    testString.utf8.enumerated().forEach { (index, byte) in
      buffer.data[index] = byte
    }

     callback.onSimpleEvent(message)
     callback.onComplexEvent(complexData)
     callback.onDataReceived(complexData, buffer)
     callback.onMaybeData(complexData)
     callback.onMultiParam(complexData, buffer, nil)
  }
}

Relevant log output

Sadly the error message doesn't really help much, just shows:

Command SwiftCompile failed with a nonzero exit code
Image

Device

iPhone 13 Pro Max

Nitro Modules Version

0.31.0

Nitrogen Version

0.31.0

Can you reproduce this issue in the Nitro Example app here?

Yes, I can reproduce the same issue in the Example app here

Additional information

ronickg avatar Oct 21 '25 01:10 ronickg

Hi Ronald!

Sadly the error message doesn't really help much, just shows

Command SwiftCompile failed with a nonzero exit code

Please read the Troubleshooting guide - this tells you how to get the actual error message.

Reproduceable Code

Repo: https://github.com/ronickg/bug

Typically the way I fix bugs is if there is a PR to this repo here that adds the change (in your case a complex callback) to the example HybridObject (here TestObject.nitro.ts) so we can all see the build fail in this specific example app. Much easier to work with than external repos.

It's hard for me to pinpoint where exactly the issue is coming from if you have 8 different functions in your code, so please do me a favor and find out which exact function it is by removing code step by step until it builds.

mrousavy avatar Oct 21 '25 09:10 mrousavy

I tried to narrow down the issue and make the sample as simple as possible. If one comments out the self.callback = callback the build will work again, or if one switches out the ArrayBuffer with a string type in the sepcs it works as well. More complex types like custom types or ArrayBuffers seem to have issues.

PR: https://github.com/mrousavy/nitro/pull/976

Error message:

1.	Apple Swift version 6.1.2 (swiftlang-6.1.2.1.2 clang-1700.0.13.5)
2.	Compiling with effective version 5.10
3.	While evaluating request IRGenRequest(IR Generation for file "/Volumes/AppleStorage/junk/nitro/packages/react-native-nitro-test/ios/HybridCallbackBuilder.swift")
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0  swift-frontend           0x0000000106fb6e24 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 56
1  swift-frontend           0x0000000106fb4c5c llvm::sys::RunSignalHandlers() + 112
2  swift-frontend           0x0000000106fb7460 SignalHandler(int) + 360
3  libsystem_platform.dylib 0x00000001824f6744 _sigtramp + 56
4  swift-frontend           0x000000010128ef20 swift::irgen::IRGenModule::getAddrOfLLVMVariableOrGOTEquivalent(swift::irgen::LinkEntity) + 388
5  swift-frontend           0x00000001012a3a4c swift::irgen::IRGenModule::getTypeEntityReference(swift::GenericTypeDecl*) + 300
6  swift-frontend           0x00000001012a5174 swift::irgen::IRGenModule::emitTypeMetadataRecords(bool)::$_1::operator()(llvm::ArrayRef<swift::GenericTypeDecl*>, llvm::StringRef) const + 124
7  swift-frontend           0x000000010128e698 swift::irgen::IRGenModule::emitTypeMetadataRecords(bool) + 936
8  swift-frontend           0x000000010141fd9c swift::IRGenRequest::evaluate(swift::Evaluator&, swift::IRGenDescriptor) const + 4576
9  swift-frontend           0x000000010146d5c4 swift::SimpleRequest<swift::IRGenRequest, swift::GeneratedModule (swift::IRGenDescriptor), (swift::RequestFlags)17>::evaluateRequest(swift::IRGenRequest const&, swift::Evaluator&) + 180
10 swift-frontend           0x00000001014291b0 swift::IRGenRequest::OutputType swift::Evaluator::getResultUncached<swift::IRGenRequest, swift::IRGenRequest::OutputType swift::evaluateOrFatal<swift::IRGenRequest>(swift::Evaluator&, swift::IRGenRequest)::'lambda'()>(swift::IRGenRequest const&, swift::IRGenRequest::OutputType swift::evaluateOrFatal<swift::IRGenRequest>(swift::Evaluator&, swift::IRGenRequest)::'lambda'()) + 812
11 swift-frontend           0x0000000101422910 swift::performIRGeneration(swift::FileUnit*, swift::IRGenOptions const&, swift::TBDGenOptions const&, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule>>, llvm::StringRef, swift::PrimarySpecificPaths const&, llvm::StringRef, llvm::GlobalVariable**) + 176
12 swift-frontend           0x0000000100e0daf0 generateIR(swift::IRGenOptions const&, swift::TBDGenOptions const&, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule>>, swift::PrimarySpecificPaths const&, llvm::StringRef, llvm::PointerUnion<swift::ModuleDecl*, swift::SourceFile*>, llvm::GlobalVariable*&, llvm::ArrayRef<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>) + 156
13 swift-frontend           0x0000000100e0907c performCompileStepsPostSILGen(swift::CompilerInstance&, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule>>, llvm::PointerUnion<swift::ModuleDecl*, swift::SourceFile*>, swift::PrimarySpecificPaths const&, int&, swift::FrontendObserver*) + 2108
14 swift-frontend           0x0000000100e080a8 swift::performCompileStepsPostSema(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 1036
15 swift-frontend           0x0000000100e0b654 performCompile(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 1764
16 swift-frontend           0x0000000100e09fd8 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 3716
17 swift-frontend           0x0000000100d8e0bc swift::mainEntry(int, char const**) + 5428
18 dyld                     0x000000018212dd54 start + 7184
Failed frontend command:

ronickg avatar Oct 21 '25 13:10 ronickg

This is a Swift bug.

Usually I do workarounds for those kinds of things but this is a bit more tricky.

Just to be sure; it does work if you store the callback - so not the additional struct you create - but the callback directly as a property?

- private let callback: MyCallback
+ private let arrayBufferCallback: (ArrayBuffer) -> Void

(which is btw recommended anyways as some languages have overhead for extra structs)

mrousavy avatar Oct 21 '25 16:10 mrousavy

If i have just a single callback so remove the onSimpleEvent then it works.

import type { HybridObject } from 'react-native-nitro-modules';
export interface MyCallback {
  // onSimpleEvent(message: string): void;
  onMaybeData(data: ArrayBuffer): void;
}

export interface CallbackTester
  extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
  createBuilder(callback: MyCallback): CallbackBuilder;
}

export interface CallbackBuilder
  extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {}

Also tried without creating an extra struct likes so, but also only works if there is a single callback:

import type { HybridObject } from 'react-native-nitro-modules';
// export interface MyCallback {
//   // onSimpleEvent(message: string): void;
//   onMaybeData(data: ArrayBuffer): void;
// }

export interface CallbackTester
  extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
  createBuilder(
    callback: (message: string) => void,
    onMaybeData: (data: ArrayBuffer) => void
  ): CallbackBuilder;
}

export interface CallbackBuilder
  extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {}

ronickg avatar Oct 21 '25 16:10 ronickg