UNPKG

nitro-codegen

Version:

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

855 lines (854 loc) 35.3 kB
import { indent } from '../../utils.js'; import { getHybridObjectName } from '../getHybridObjectName.js'; import { getReferencedTypes } from '../getReferencedTypes.js'; import { ArrayType } from '../types/ArrayType.js'; import { EnumType } from '../types/EnumType.js'; import { FunctionType } from '../types/FunctionType.js'; import { getTypeAs } from '../types/getTypeAs.js'; import { HybridObjectType } from '../types/HybridObjectType.js'; import { OptionalType } from '../types/OptionalType.js'; import { PromiseType } from '../types/PromiseType.js'; import { RecordType } from '../types/RecordType.js'; import { StructType } from '../types/StructType.js'; import { VariantType } from '../types/VariantType.js'; import { getKotlinBoxedPrimitiveType } from './KotlinBoxedPrimitive.js'; import { createKotlinEnum } from './KotlinEnum.js'; import { createKotlinFunction } from './KotlinFunction.js'; import { createKotlinStruct } from './KotlinStruct.js'; import { createKotlinVariant } from './KotlinVariant.js'; export class KotlinCxxBridgedType { type; constructor(type) { this.type = type; } get hasType() { return this.type.kind !== 'void' && this.type.kind !== 'null'; } get canBePassedByReference() { return this.type.canBePassedByReference; } get needsSpecialHandling() { switch (this.type.kind) { case 'function': // Function needs to be converted from JFunc_... to Lambda return true; default: break; } // check if any types this type references (e.g. underlying optional, array element, ...) // needs special handling. if yes, we need it as well const referencedTypes = getReferencedTypes(this.type) .filter((t) => t !== this.type) .map((t) => new KotlinCxxBridgedType(t)); for (const type of referencedTypes) { if (type.needsSpecialHandling) { return true; } } // no special handling needed return false; } getRequiredImports() { const imports = this.type.getRequiredImports(); switch (this.type.kind) { case 'enum': const enumType = getTypeAs(this.type, EnumType); imports.push({ language: 'c++', name: `J${enumType.enumName}.hpp`, space: 'user', }); break; case 'struct': const structType = getTypeAs(this.type, StructType); imports.push({ language: 'c++', name: `J${structType.structName}.hpp`, space: 'user', }); break; case 'function': const functionType = getTypeAs(this.type, FunctionType); imports.push({ language: 'c++', name: `J${functionType.specializationName}.hpp`, space: 'user', }); break; case 'array-buffer': imports.push({ language: 'c++', name: 'NitroModules/JArrayBuffer.hpp', space: 'system', }); imports.push({ language: 'c++', name: 'NitroModules/JUnit.hpp', space: 'system', }); break; case 'promise': imports.push({ language: 'c++', name: 'NitroModules/JPromise.hpp', space: 'system', }); break; case 'date': imports.push({ language: 'c++', name: 'NitroModules/JInstant.hpp', space: 'system', }); break; case 'map': imports.push({ language: 'c++', name: 'NitroModules/JAnyMap.hpp', space: 'system', }); break; case 'variant': const variantType = getTypeAs(this.type, VariantType); const variantName = variantType.getAliasName('kotlin'); imports.push({ language: 'c++', name: `J${variantName}.hpp`, space: 'user', }); break; case 'hybrid-object': { const hybridObjectType = getTypeAs(this.type, HybridObjectType); const name = getHybridObjectName(hybridObjectType.hybridObjectName); imports.push({ language: 'c++', name: `${name.JHybridTSpec}.hpp`, space: 'user', }); imports.push({ language: 'c++', name: 'NitroModules/JNISharedPtr.hpp', space: 'system', }); break; } } // Recursively look into referenced types (e.g. the `T` of a `optional<T>`, or `T` of a `T[]`) const referencedTypes = getReferencedTypes(this.type); referencedTypes.forEach((t) => { if (t === this.type) { // break a recursion - we already know this type return; } const bridged = new KotlinCxxBridgedType(t); imports.push(...bridged.getRequiredImports()); }); return imports; } getExtraFiles() { const files = []; switch (this.type.kind) { case 'enum': const enumType = getTypeAs(this.type, EnumType); const enumFiles = createKotlinEnum(enumType); files.push(...enumFiles); break; case 'struct': const structType = getTypeAs(this.type, StructType); const structFiles = createKotlinStruct(structType); files.push(...structFiles); break; case 'variant': const variantType = getTypeAs(this.type, VariantType); const variantFiles = createKotlinVariant(variantType); files.push(...variantFiles); break; case 'function': const functionType = getTypeAs(this.type, FunctionType); const funcFiles = createKotlinFunction(functionType); files.push(...funcFiles); break; } // Recursively look into referenced types (e.g. the `T` of a `optional<T>`, or `T` of a `T[]`) const referencedTypes = getReferencedTypes(this.type); referencedTypes.forEach((t) => { if (t === this.type) { // break a recursion - we already know this type return; } const bridged = new KotlinCxxBridgedType(t); files.push(...bridged.getExtraFiles()); }); return files; } asJniReferenceType(referenceType = 'alias') { switch (this.type.kind) { case 'void': case 'number': case 'boolean': case 'bigint': // primitives are not references return this.getTypeCode('c++'); default: return `jni::${referenceType}_ref<${this.getTypeCode('c++')}>`; } } getTypeCode(language, isBoxed = false) { switch (this.type.kind) { case 'number': case 'bigint': case 'boolean': if (isBoxed) { return getKotlinBoxedPrimitiveType(this.type); } else { if (this.type.kind === 'boolean' && language === 'c++') { // JNI does not use `bool`, so it is a jboolean instead. return 'jboolean'; } return this.type.getCode(language); } case 'array': const array = getTypeAs(this.type, ArrayType); const bridgedItem = new KotlinCxxBridgedType(array.itemType); switch (language) { case 'c++': switch (array.itemType.kind) { case 'number': return 'jni::JArrayDouble'; case 'boolean': return 'jni::JArrayBoolean'; case 'bigint': return 'jni::JArrayLong'; default: return `jni::JArrayClass<${bridgedItem.getTypeCode(language)}>`; } case 'kotlin': return `Array<${bridgedItem.getTypeCode(language)}>`; default: return this.type.getCode(language); } case 'string': switch (language) { case 'c++': return 'jni::JString'; default: return this.type.getCode(language); } case 'record': { switch (language) { case 'c++': const recordType = getTypeAs(this.type, RecordType); const keyType = new KotlinCxxBridgedType(recordType.keyType).getTypeCode(language); const valueType = new KotlinCxxBridgedType(recordType.valueType).getTypeCode(language); return `jni::JMap<${keyType}, ${valueType}>`; default: return this.type.getCode(language); } } case 'enum': switch (language) { case 'c++': const enumType = getTypeAs(this.type, EnumType); return `J${enumType.enumName}`; default: return this.type.getCode(language); } case 'struct': switch (language) { case 'c++': const structType = getTypeAs(this.type, StructType); return `J${structType.structName}`; default: return this.type.getCode(language); } case 'function': const functionType = getTypeAs(this.type, FunctionType); switch (language) { case 'c++': return `J${functionType.specializationName}::javaobject`; case 'kotlin': return functionType.specializationName; default: return this.type.getCode(language); } case 'hybrid-object': { switch (language) { case 'c++': const hybridObjectType = getTypeAs(this.type, HybridObjectType); const name = getHybridObjectName(hybridObjectType.hybridObjectName); return `${name.JHybridTSpec}::javaobject`; default: return this.type.getCode(language); } } case 'array-buffer': switch (language) { case 'c++': return `JArrayBuffer::javaobject`; default: return this.type.getCode(language); } case 'date': switch (language) { case 'c++': return `JInstant`; default: return this.type.getCode(language); } case 'variant': { const variant = getTypeAs(this.type, VariantType); const name = variant.getAliasName('kotlin'); switch (language) { case 'c++': return `J${name}`; case 'kotlin': return name; default: return this.type.getCode(language); } } case 'promise': switch (language) { case 'c++': return `JPromise::javaobject`; default: return this.type.getCode(language); } case 'map': switch (language) { case 'c++': return `JAnyMap::javaobject`; default: return this.type.getCode(language); } case 'optional': { const optional = getTypeAs(this.type, OptionalType); const bridgedWrappingType = new KotlinCxxBridgedType(optional.wrappingType); switch (language) { case 'c++': switch (optional.wrappingType.kind) { // primitives need to be boxed to make them nullable case 'number': case 'boolean': case 'bigint': const boxed = getKotlinBoxedPrimitiveType(optional.wrappingType); return boxed; default: // all other types can be nullable as they are objects. return bridgedWrappingType.getTypeCode('c++'); } case 'kotlin': return `${bridgedWrappingType.getTypeCode(language)}?`; default: return this.type.getCode(language); } } case 'error': switch (language) { case 'c++': return 'jni::JThrowable'; default: return this.type.getCode(language); } default: return this.type.getCode(language); } } parse(parameterName, from, to, inLanguage) { if (from === 'c++') { return this.parseFromCppToKotlin(parameterName, inLanguage); } else if (from === 'kotlin') { return this.parseFromKotlinToCpp(parameterName, inLanguage); } else { throw new Error(`Cannot parse "${parameterName}" from ${from} to ${to}!`); } } parseFromCppToKotlin(parameterName, language, isBoxed = false) { switch (this.type.kind) { case 'number': case 'boolean': case 'bigint': switch (language) { case 'c++': if (isBoxed) { // box a primitive (double) to an object (JDouble) const boxed = getKotlinBoxedPrimitiveType(this.type); return `${boxed}::valueOf(${parameterName})`; } else { return parameterName; } default: return parameterName; } case 'string': switch (language) { case 'c++': return `jni::make_jstring(${parameterName})`; default: return parameterName; } case 'struct': { switch (language) { case 'c++': const struct = getTypeAs(this.type, StructType); return `J${struct.structName}::fromCpp(${parameterName})`; default: return parameterName; } } case 'variant': { switch (language) { case 'c++': const variant = getTypeAs(this.type, VariantType); const name = variant.getAliasName('kotlin'); return `J${name}::fromCpp(${parameterName})`; default: return parameterName; } } case 'date': { switch (language) { case 'c++': return `JInstant::fromChrono(${parameterName})`; default: return parameterName; } } case 'enum': { switch (language) { case 'c++': const enumType = getTypeAs(this.type, EnumType); return `J${enumType.enumName}::fromCpp(${parameterName})`; default: return parameterName; } } case 'function': { switch (language) { case 'c++': const func = getTypeAs(this.type, FunctionType); return `J${func.specializationName}_cxx::fromCpp(${parameterName})`; case 'kotlin': return parameterName; default: return parameterName; } } case 'hybrid-object': { switch (language) { case 'c++': const hybrid = getTypeAs(this.type, HybridObjectType); const name = getHybridObjectName(hybrid.hybridObjectName); return `std::dynamic_pointer_cast<${name.JHybridTSpec}>(${parameterName})->getJavaPart()`; default: return parameterName; } } case 'optional': { const optional = getTypeAs(this.type, OptionalType); const bridge = new KotlinCxxBridgedType(optional.wrappingType); switch (language) { case 'c++': return `${parameterName}.has_value() ? ${bridge.parseFromCppToKotlin(`${parameterName}.value()`, 'c++', true)} : nullptr`; case 'kotlin': if (bridge.needsSpecialHandling) { return `${parameterName}?.let { ${bridge.parseFromCppToKotlin('it', language, isBoxed)} }`; } else { return parameterName; } default: return parameterName; } } case 'array-buffer': { switch (language) { case 'c++': return `JArrayBuffer::wrap(${parameterName})`; default: return parameterName; } } case 'map': { switch (language) { case 'c++': return `JAnyMap::create(${parameterName})`; default: return parameterName; } } case 'record': { switch (language) { case 'c++': const record = getTypeAs(this.type, RecordType); const key = new KotlinCxxBridgedType(record.keyType); const value = new KotlinCxxBridgedType(record.valueType); const parseKey = key.parseFromCppToKotlin('__entry.first', 'c++'); const parseValue = value.parseFromCppToKotlin('__entry.second', 'c++'); const javaMapType = `jni::JMap<${key.getTypeCode('c++')}, ${value.getTypeCode('c++')}>`; const javaHashMapType = `jni::JHashMap<${key.getTypeCode('c++')}, ${value.getTypeCode('c++')}>`; return ` [&]() -> jni::local_ref<${javaMapType}> { auto __map = ${javaHashMapType}::create(${parameterName}.size()); for (const auto& __entry : ${parameterName}) { __map->put(${indent(parseKey, ' ')}, ${indent(parseValue, ' ')}); } return __map; }() `.trim(); default: return parameterName; } } case 'array': { const array = getTypeAs(this.type, ArrayType); const arrayType = this.getTypeCode('c++'); const bridge = new KotlinCxxBridgedType(array.itemType); switch (language) { case 'c++': { switch (array.itemType.kind) { case 'number': case 'boolean': case 'bigint': { // primitive arrays can be constructed more efficiently with region/batch access. // no need to iterate through the entire array. return ` [&]() { size_t __size = ${parameterName}.size(); jni::local_ref<${arrayType}> __array = ${arrayType}::newArray(__size); __array->setRegion(0, __size, ${parameterName}.data()); return __array; }() `.trim(); } default: { // other arrays need to loop through return ` [&]() { size_t __size = ${parameterName}.size(); jni::local_ref<${arrayType}> __array = ${arrayType}::newArray(__size); for (size_t __i = 0; __i < __size; __i++) { const auto& __element = ${parameterName}[__i]; __array->setElement(__i, *${indent(bridge.parseFromCppToKotlin('__element', 'c++'), ' ')}); } return __array; }() `.trim(); } } } case 'kotlin': if (bridge.needsSpecialHandling) { return `${parameterName}.map { ${bridge.parseFromCppToKotlin('it', language, isBoxed)} }`; } else { return parameterName; } default: return parameterName; } } case 'promise': { switch (language) { case 'c++': { const promise = getTypeAs(this.type, PromiseType); const resolvingType = promise.resultingType.getCode('c++'); const bridge = new KotlinCxxBridgedType(promise.resultingType); if (promise.resultingType.kind === 'void') { // void: resolve() return ` [&]() { jni::local_ref<JPromise::javaobject> __localPromise = JPromise::create(); jni::global_ref<JPromise::javaobject> __promise = jni::make_global(__localPromise); ${parameterName}->addOnResolvedListener([=]() { __promise->cthis()->resolve(JUnit::instance()); }); ${parameterName}->addOnRejectedListener([=](const std::exception_ptr& __error) { auto __jniError = jni::getJavaExceptionForCppException(__error); __promise->cthis()->reject(__jniError); }); return __localPromise; }() `.trim(); } else { // T: resolve(T) return ` [&]() { jni::local_ref<JPromise::javaobject> __localPromise = JPromise::create(); jni::global_ref<JPromise::javaobject> __promise = jni::make_global(__localPromise); ${parameterName}->addOnResolvedListener([=](const ${resolvingType}& __result) { __promise->cthis()->resolve(${indent(bridge.parseFromCppToKotlin('__result', 'c++', true), ' ')}); }); ${parameterName}->addOnRejectedListener([=](const std::exception_ptr& __error) { auto __jniError = jni::getJavaExceptionForCppException(__error); __promise->cthis()->reject(__jniError); }); return __localPromise; }() `.trim(); } } default: return parameterName; } } case 'error': switch (language) { case 'c++': return `jni::getJavaExceptionForCppException(${parameterName})`; default: return parameterName; } default: // no need to parse anything, just return as is return parameterName; } } parseFromKotlinToCpp(parameterName, language, isBoxed = false) { switch (this.type.kind) { case 'number': case 'boolean': case 'bigint': switch (language) { case 'c++': let code; if (isBoxed) { // unbox an object (JDouble) to a primitive (double) code = `${parameterName}->value()`; } else { // it's just the primitive type directly code = parameterName; } if (this.type.kind === 'boolean') { // jboolean =/= bool (it's a char in Java) code = `static_cast<bool>(${code})`; } return code; default: return parameterName; } case 'string': switch (language) { case 'c++': return `${parameterName}->toStdString()`; default: return parameterName; } case 'struct': case 'enum': { switch (language) { case 'c++': return `${parameterName}->toCpp()`; default: return parameterName; } } case 'variant': { switch (language) { case 'c++': return `${parameterName}->toCpp()`; default: return parameterName; } } case 'date': { switch (language) { case 'c++': return `${parameterName}->toChrono()`; default: return parameterName; } } case 'hybrid-object': { switch (language) { case 'c++': const hybrid = getTypeAs(this.type, HybridObjectType); const name = getHybridObjectName(hybrid.hybridObjectName); return `JNISharedPtr::make_shared_from_jni<${name.JHybridTSpec}>(jni::make_global(${parameterName}))`; default: return parameterName; } } case 'optional': { const optional = getTypeAs(this.type, OptionalType); const bridge = new KotlinCxxBridgedType(optional.wrappingType); switch (language) { case 'c++': const parsed = bridge.parseFromKotlinToCpp(parameterName, 'c++', true); return `${parameterName} != nullptr ? std::make_optional(${parsed}) : std::nullopt`; case 'kotlin': if (bridge.needsSpecialHandling) { return `${parameterName}?.let { ${bridge.parseFromKotlinToCpp('it', language, isBoxed)} }`; } else { return parameterName; } default: return parameterName; } } case 'function': { const functionType = getTypeAs(this.type, FunctionType); switch (language) { case 'c++': { const returnType = functionType.returnType.getCode('c++'); const params = functionType.parameters.map((p) => `${p.getCode('c++')} ${p.escapedName}`); const paramsForward = functionType.parameters.map((p) => p.escapedName); const jniType = `J${functionType.specializationName}_cxx`; return ` [&]() -> ${functionType.getCode('c++')} { if (${parameterName}->isInstanceOf(${jniType}::javaClassStatic())) [[likely]] { auto downcast = jni::static_ref_cast<${jniType}::javaobject>(${parameterName}); return downcast->cthis()->getFunction(); } else { auto ${parameterName}Ref = jni::make_global(${parameterName}); return [${parameterName}Ref](${params.join(', ')}) -> ${returnType} { return ${parameterName}Ref->invoke(${paramsForward}); }; } }() `.trim(); } case 'kotlin': { return `${functionType.specializationName}_java(${parameterName})`; } default: return parameterName; } } case 'array-buffer': { switch (language) { case 'c++': return `${parameterName}->cthis()->getArrayBuffer()`; default: return parameterName; } } case 'map': { switch (language) { case 'c++': return `${parameterName}->cthis()->getMap()`; default: return parameterName; } } case 'record': { switch (language) { case 'c++': const record = getTypeAs(this.type, RecordType); const key = new KotlinCxxBridgedType(record.keyType); const value = new KotlinCxxBridgedType(record.valueType); const parseKey = key.parseFromKotlinToCpp('__entry.first', 'c++'); const parseValue = value.parseFromKotlinToCpp('__entry.second', 'c++'); const cxxType = this.type.getCode('c++'); return ` [&]() { ${cxxType} __map; __map.reserve(${parameterName}->size()); for (const auto& __entry : *${parameterName}) { __map.emplace(${indent(parseKey, ' ')}, ${indent(parseValue, ' ')}); } return __map; }() `.trim(); default: return parameterName; } } case 'array': { switch (language) { case 'c++': const array = getTypeAs(this.type, ArrayType); const bridge = new KotlinCxxBridgedType(array.itemType); const itemType = array.itemType.getCode('c++'); switch (array.itemType.kind) { case 'number': case 'boolean': case 'bigint': { // primitive arrays can use region/batch access, // which we can use to construct the vector directly instead of looping through it. return ` [&]() { size_t __size = ${parameterName}->size(); std::vector<${itemType}> __vector(__size); ${parameterName}->getRegion(0, __size, __vector.data()); return __vector; }() `.trim(); } default: { // other arrays need to loop through return ` [&]() { size_t __size = ${parameterName}->size(); std::vector<${itemType}> __vector; __vector.reserve(__size); for (size_t __i = 0; __i < __size; __i++) { auto __element = ${parameterName}->getElement(__i); __vector.push_back(${bridge.parseFromKotlinToCpp('__element', 'c++')}); } return __vector; }() `.trim(); } } default: return parameterName; } } case 'promise': { switch (language) { case 'c++': const promise = getTypeAs(this.type, PromiseType); const actualCppType = promise.resultingType.getCode('c++'); const resultingType = new KotlinCxxBridgedType(promise.resultingType); let resolveBody; if (resultingType.hasType) { // it's a Promise<T> resolveBody = ` auto __result = jni::static_ref_cast<${resultingType.getTypeCode('c++', true)}>(__boxedResult); __promise->resolve(${resultingType.parseFromKotlinToCpp('__result', 'c++', true)}); `.trim(); } else { // it's a Promise<void> resolveBody = ` __promise->resolve(); `.trim(); } return ` [&]() { auto __promise = Promise<${actualCppType}>::create(); ${parameterName}->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& ${resultingType.hasType ? '__boxedResult' : '/* unit */'}) { ${indent(resolveBody, ' ')} }); ${parameterName}->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) { jni::JniException __jniError(__throwable); __promise->reject(std::make_exception_ptr(__jniError)); }); return __promise; }() `.trim(); default: return parameterName; } } case 'error': switch (language) { case 'c++': return `jni::JniException(${parameterName})`; default: return parameterName; } default: // no need to parse anything, just return as is return parameterName; } } }