nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
437 lines (407 loc) • 16.6 kB
JavaScript
import { SwiftCxxBridgedType } from './SwiftCxxBridgedType.js';
import { indent } from '../../utils.js';
import { createFileMetadataString, escapeCppName, isNotDuplicate, } from '../helpers.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) {
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;
let setter;
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;
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 = [];
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) {
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) {
// 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;
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();
}