nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
901 lines (882 loc) • 29.2 kB
text/typescript
import { indent } from '../../utils.js'
import type { BridgedType } from '../BridgedType.js'
import { getHybridObjectName } from '../getHybridObjectName.js'
import { getReferencedTypes } from '../getReferencedTypes.js'
import type { SourceFile, SourceImport } from '../SourceFile.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 type { Type } from '../types/Type.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 implements BridgedType<'kotlin', 'c++'> {
readonly type: Type
constructor(type: Type) {
this.type = type
}
get hasType(): boolean {
return this.type.kind !== 'void' && this.type.kind !== 'null'
}
get canBePassedByReference(): boolean {
return this.type.canBePassedByReference
}
get needsSpecialHandling(): boolean {
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(): SourceImport[] {
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(): SourceFile[] {
const files: SourceFile[] = []
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' | 'local' | 'global' = '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: 'kotlin' | 'c++', isBoxed = false): string {
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: string,
from: 'c++' | 'kotlin',
to: 'kotlin' | 'c++',
inLanguage: 'kotlin' | 'c++'
): string {
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: string,
language: 'kotlin' | 'c++',
isBoxed = false
): string {
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: string,
language: 'kotlin' | 'c++',
isBoxed = false
): string {
switch (this.type.kind) {
case 'number':
case 'boolean':
case 'bigint':
switch (language) {
case 'c++':
let code: string
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: string
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
}
}
}