UNPKG

nitro-codegen

Version:

The code-generator for react-native-nitro-modules.

502 lines (444 loc) 16.5 kB
import type { HybridObjectSpec } from '../HybridObjectSpec.js' import { SwiftCxxBridgedType } from './SwiftCxxBridgedType.js' import type { Property } from '../Property.js' import { indent } from '../../utils.js' import type { Method } from '../Method.js' import { createFileMetadataString, escapeCppName, isNotDuplicate, } from '../helpers.js' import type { SourceFile } from '../SourceFile.js' import { getHybridObjectName } from '../getHybridObjectName.js' import { getForwardDeclaration } from '../c++/getForwardDeclaration.js' import { NitroConfig } from '../../config/NitroConfig.js' import { includeHeader } from '../c++/includeNitroHeader.js' import { getUmbrellaHeaderName } from '../../autolinking/ios/createSwiftUmbrellaHeader.js' import { HybridObjectType } from '../types/HybridObjectType.js' import { addKnownType } from '../createType.js' import { ResultWrappingType } from '../types/ResultWrappingType.js' export function getBridgeNamespace() { return NitroConfig.getCxxNamespace('swift', 'bridge', 'swift') } /** * Creates a Swift class that bridges Swift over to C++. * We need this because not all Swift types are accessible in C++, and vice versa. * * For example, Enums need to be converted to Int32 (because of a Swift compiler bug), * Promise<..> has to be converted to a Promise<..>, exceptions have to be handled * via custom Result types, etc.. */ export function createSwiftHybridObjectCxxBridge( spec: HybridObjectSpec ): SourceFile[] { const name = getHybridObjectName(spec.name) const moduleName = NitroConfig.getIosModuleName() const propertiesBridge = spec.properties.map((p) => getPropertyForwardImplementation(p) ) const methodsBridge = spec.methods.map((m) => getMethodForwardImplementation(m) ) const baseClasses = spec.baseTypes.map((base) => { const baseName = getHybridObjectName(base.name) return baseName.HybridTSpecCxx }) const hasBase = baseClasses.length > 0 if (spec.isHybridView && !hasBase) { methodsBridge.push( ` public final func getView() -> UnsafeMutableRawPointer { return Unmanaged.passRetained(__implementation.view).toOpaque() } `.trim(), ` public final func beforeUpdate() { __implementation.beforeUpdate() } `.trim(), ` public final func afterUpdate() { __implementation.afterUpdate() } `.trim() ) } const hybridObject = new HybridObjectType(spec) const bridgedType = new SwiftCxxBridgedType(hybridObject) const bridge = bridgedType.getRequiredBridge() if (bridge == null) throw new Error(`HybridObject Type should have a bridge!`) const weakifyBridge = bridge.dependencies.find((d) => d.funcName.startsWith('weakify') ) if (weakifyBridge == null) throw new Error( `HybridObject ${spec.name} does not have a weakify_..() bridge!` ) const cppWeakPtrName = escapeCppName(hybridObject.getCode('c++', 'weak')) const baseGetCxxPartOverrides = spec.baseTypes.map((base) => { const baseHybridObject = new HybridObjectType(base) const bridgedBase = new SwiftCxxBridgedType(baseHybridObject) const baseBridge = bridgedBase.getRequiredBridge() if (baseBridge == null) throw new Error(`HybridObject ${base.name}'s bridge cannot be null!`) const upcastBridge = bridge.dependencies.find( (b) => b.funcName.includes(escapeCppName(spec.name)) && b.funcName.includes(escapeCppName(base.name)) ) if (upcastBridge == null) throw new Error( `HybridObject ${spec.name}'s upcast-bridge cannot be found! ${JSON.stringify(baseBridge)}` ) return ` public override func getCxxPart() -> bridge.${baseBridge.specializationName} { let ownCxxPart: bridge.${bridge.specializationName} = getCxxPart() return bridge.${upcastBridge.funcName}(ownCxxPart) }`.trim() }) const swiftCxxWrapperCode = ` ${createFileMetadataString(`${name.HybridTSpecCxx}.swift`)} import Foundation import NitroModules /** * A class implementation that bridges ${name.HybridTSpec} over to C++. * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. * * Also, some Swift types need to be bridged with special handling: * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper * - Throwing methods need to be wrapped with a Result<T, Error> type, as exceptions cannot be propagated to C++ */ ${hasBase ? `public class ${name.HybridTSpecCxx} : ${baseClasses.join(', ')}` : `public class ${name.HybridTSpecCxx}`} { /** * The Swift <> C++ bridge's namespace (\`${NitroConfig.getCxxNamespace('c++', 'bridge', 'swift')}\`) * from \`${moduleName}-Swift-Cxx-Bridge.hpp\`. * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. */ public typealias bridge = ${getBridgeNamespace()} /** * Holds an instance of the \`${name.HybridTSpec}\` Swift protocol. */ private var __implementation: any ${name.HybridTSpec} /** * Holds a weak pointer to the C++ class that wraps the Swift class. */ private var __cxxPart: bridge.${cppWeakPtrName} /** * Create a new \`${name.HybridTSpecCxx}\` that wraps the given \`${name.HybridTSpec}\`. * All properties and methods bridge to C++ types. */ public init(_ implementation: any ${name.HybridTSpec}) { self.__implementation = implementation self.__cxxPart = .init() ${hasBase ? 'super.init(implementation)' : '/* no base class */'} } /** * Get the actual \`${name.HybridTSpec}\` instance this class wraps. */ @inline(__always) public func get${name.HybridTSpec}() -> any ${name.HybridTSpec} { return __implementation } /** * Casts this instance to a retained unsafe raw pointer. * This acquires one additional strong reference on the object! */ public ${hasBase ? 'override func' : 'func'} toUnsafe() -> UnsafeMutableRawPointer { return Unmanaged.passRetained(self).toOpaque() } /** * Casts an unsafe pointer to a \`${name.HybridTSpecCxx}\`. * The pointer has to be a retained opaque \`Unmanaged<${name.HybridTSpecCxx}>\`. * This removes one strong reference from the object! */ public ${hasBase ? 'override class func' : 'class func'} fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> ${name.HybridTSpecCxx} { return Unmanaged<${name.HybridTSpecCxx}>.fromOpaque(pointer).takeRetainedValue() } /** * Gets (or creates) the C++ part of this Hybrid Object. * The C++ part is a \`${bridge.cxxType}\`. */ public func getCxxPart() -> bridge.${bridge.specializationName} { let cachedCxxPart = self.__cxxPart.lock() if cachedCxxPart.__convertToBool() { return cachedCxxPart } else { let newCxxPart = bridge.${bridge.funcName}(self.toUnsafe()) __cxxPart = bridge.${weakifyBridge.funcName}(newCxxPart) return newCxxPart } } ${indent(baseGetCxxPartOverrides.join('\n'), ' ')} /** * Get the memory size of the Swift class (plus size of any other allocations) * so the JS VM can properly track it and garbage-collect the JS object if needed. */ @inline(__always) public ${hasBase ? 'override var' : 'var'} memorySize: Int { return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize } // Properties ${indent(propertiesBridge.join('\n\n'), ' ')} // Methods ${indent(methodsBridge.join('\n\n'), ' ')} } ` const cppProperties = spec.properties .map((p) => { const bridged = new SwiftCxxBridgedType(p.type) let getter: string let setter: string const getterName = p.getGetterName('swift') const setterName = p.getSetterName('swift') if (bridged.needsSpecialHandling) { // we need custom C++ -> Swift conversion code getter = ` auto __result = _swiftPart.${getterName}(); return ${bridged.parseFromSwiftToCpp('__result', 'c++')}; ` setter = `_swiftPart.${setterName}(${bridged.parseFromCppToSwift(p.name, 'c++')});` } else { // just forward value directly getter = `return _swiftPart.${getterName}();` setter = `_swiftPart.${setterName}(std::forward<decltype(${p.name})>(${p.name}));` } return p.getCode( 'c++', { inline: true, override: true, noexcept: true }, { getter: getter.trim(), setter: setter.trim(), } ) }) .join('\n') const cppMethods = spec.methods .map((m) => { const params = m.parameters .map((p) => { const bridged = new SwiftCxxBridgedType(p.type) if (bridged.needsSpecialHandling) { // we need custom C++ -> Swift conversion code return bridged.parseFromCppToSwift(p.name, 'c++') } else { // just forward value directly return `std::forward<decltype(${p.name})>(${p.name})` } }) .join(', ') const bridgedReturnType = new SwiftCxxBridgedType(m.returnType, true) const hasResult = m.returnType.kind !== 'void' let body: string if (hasResult) { // func returns something body = ` auto __result = _swiftPart.${m.name}(${params}); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); } auto __value = std::move(__result.value()); return ${bridgedReturnType.parseFromSwiftToCpp('__value', 'c++')}; `.trim() } else { // void func body = ` auto __result = _swiftPart.${m.name}(${params}); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); } `.trim() } return m.getCode('c++', { inline: true, override: true }, body) }) .join('\n') const allBridgedTypes = [ ...spec.properties.flatMap((p) => new SwiftCxxBridgedType(p.type)), ...spec.methods.flatMap((m) => { const bridgedReturn = new SwiftCxxBridgedType(m.returnType) const bridgedParams = m.parameters.map( (p) => new SwiftCxxBridgedType(p.type) ) return [bridgedReturn, ...bridgedParams] }), ] const cxxNamespace = NitroConfig.getCxxNamespace('c++') const iosModuleName = NitroConfig.getIosModuleName() const extraImports = allBridgedTypes.flatMap((b) => b.getRequiredImports()) const cppBaseClasses = [`public virtual ${name.HybridTSpec}`] const cppBaseCtorCalls = [`HybridObject(${name.HybridTSpec}::TAG)`] for (const base of spec.baseTypes) { const baseName = getHybridObjectName(base.name) cppBaseClasses.push(`public virtual ${baseName.HybridTSpecSwift}`) cppBaseCtorCalls.push(`${baseName.HybridTSpecSwift}(swiftPart)`) extraImports.push({ language: 'c++', name: `${baseName.HybridTSpecSwift}.hpp`, space: 'user', forwardDeclaration: getForwardDeclaration( 'class', baseName.HybridTSpecSwift, cxxNamespace ), }) } const extraForwardDeclarations = extraImports .map((i) => i.forwardDeclaration) .filter((v) => v != null) .filter(isNotDuplicate) const extraIncludes = extraImports .map((i) => includeHeader(i)) .filter(isNotDuplicate) // TODO: Remove forward declaration once Swift fixes the wrong order in generated -Swift.h headers! const cppHybridObjectCode = ` ${createFileMetadataString(`${name.HybridTSpecSwift}.hpp`)} #pragma once #include "${name.HybridTSpec}.hpp" ${getForwardDeclaration('class', name.HybridTSpecCxx, iosModuleName)} ${extraForwardDeclarations.join('\n')} ${extraIncludes.join('\n')} #include "${getUmbrellaHeaderName()}" namespace ${cxxNamespace} { /** * The C++ part of ${name.HybridTSpecCxx}.swift. * * ${name.HybridTSpecSwift} (C++) accesses ${name.HybridTSpecCxx} (Swift), and might * contain some additional bridging code for C++ <> Swift interop. * * Since this obviously introduces an overhead, I hope at some point in * the future, ${name.HybridTSpecCxx} can directly inherit from the C++ class ${name.HybridTSpec} * to simplify the whole structure and memory management. */ class ${name.HybridTSpecSwift}: ${cppBaseClasses.join(', ')} { public: // Constructor from a Swift instance explicit ${name.HybridTSpecSwift}(const ${iosModuleName}::${name.HybridTSpecCxx}& swiftPart): ${indent(cppBaseCtorCalls.join(',\n'), ' ')}, _swiftPart(swiftPart) { } public: // Get the Swift part inline ${iosModuleName}::${name.HybridTSpecCxx}& getSwiftPart() noexcept { return _swiftPart; } public: // Get memory pressure inline size_t getExternalMemorySize() noexcept override { return _swiftPart.getMemorySize(); } public: // Properties ${indent(cppProperties, ' ')} public: // Methods ${indent(cppMethods, ' ')} private: ${iosModuleName}::${name.HybridTSpecCxx} _swiftPart; }; } // namespace ${cxxNamespace} ` const cppHybridObjectCodeCpp = ` ${createFileMetadataString(`${name.HybridTSpecSwift}.cpp`)} #include "${name.HybridTSpecSwift}.hpp" namespace ${cxxNamespace} { } // namespace ${cxxNamespace} ` const files: SourceFile[] = [] files.push(...allBridgedTypes.flatMap((b) => b.getExtraFiles())) files.push({ content: swiftCxxWrapperCode, language: 'swift', name: `${name.HybridTSpecCxx}.swift`, subdirectory: [], platform: 'ios', }) files.push({ content: cppHybridObjectCode, language: 'c++', name: `${name.HybridTSpecSwift}.hpp`, subdirectory: [], platform: 'ios', }) files.push({ content: cppHybridObjectCodeCpp, language: 'c++', name: `${name.HybridTSpecSwift}.cpp`, subdirectory: [], platform: 'ios', }) return files } function getPropertyForwardImplementation(property: Property): string { const bridgedType = new SwiftCxxBridgedType(property.type) const convertToCpp = bridgedType.parseFromSwiftToCpp( `self.__implementation.${property.name}`, 'swift' ) const convertFromCpp = bridgedType.parseFromCppToSwift('newValue', 'swift') const getter = ` @inline(__always) get { return ${indent(convertToCpp, ' ')} } `.trim() const setter = ` @inline(__always) set { self.__implementation.${property.name} = ${indent(convertFromCpp, ' ')} } `.trim() const body = [getter] if (!property.isReadonly) { body.push(setter) } const code = ` public final var ${property.name}: ${bridgedType.getTypeCode('swift')} { ${indent(body.join('\n'), ' ')} } ` return code.trim() } function getMethodForwardImplementation(method: Method): string { // wrapped return in a std::expected const resultType = new ResultWrappingType(method.returnType) addKnownType(`expected_${resultType.getCode('c++')}`, resultType, 'swift') const bridgedResultType = new SwiftCxxBridgedType(resultType, true) const resultBridge = bridgedResultType.getRequiredBridge() if (resultBridge == null) throw new Error( `Result type (${bridgedResultType.getTypeCode('c++')}) does not have a bridge!` ) const bridgedErrorType = new SwiftCxxBridgedType(resultType.error, true) const returnType = new SwiftCxxBridgedType(method.returnType, true) const params = method.parameters.map((p) => { const bridgedType = new SwiftCxxBridgedType(p.type) return `${p.name}: ${bridgedType.getTypeCode('swift')}` }) const passParams = method.parameters.map((p) => { const bridgedType = new SwiftCxxBridgedType(p.type) return `${p.name}: ${bridgedType.parseFromCppToSwift(p.name, 'swift')}` }) let body: string if (returnType.hasType) { body = ` let __result = try self.__implementation.${method.name}(${passParams.join(', ')}) let __resultCpp = ${returnType.parseFromSwiftToCpp('__result', 'swift')} return bridge.${resultBridge.funcName}(__resultCpp) `.trim() } else { body = ` try self.__implementation.${method.name}(${passParams.join(', ')}) return bridge.${resultBridge.funcName}() `.trim() } return ` @inline(__always) public final func ${method.name}(${params.join(', ')}) -> ${bridgedResultType.getTypeCode('swift')} { do { ${indent(body, ' ')} } catch (let __error) { let __exceptionPtr = ${indent(bridgedErrorType.parseFromSwiftToCpp('__error', 'swift'), ' ')} return bridge.${resultBridge.funcName}(__exceptionPtr) } } `.trim() }