nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
195 lines (174 loc) • 6.05 kB
text/typescript
import { NitroConfig } from '../../config/NitroConfig.js'
import { capitalizeName, indent } from '../../utils.js'
import { includeHeader } from '../c++/includeNitroHeader.js'
import { getReferencedTypes } from '../getReferencedTypes.js'
import { createFileMetadataString, isNotDuplicate } from '../helpers.js'
import type { SourceFile } from '../SourceFile.js'
import type { StructType } from '../types/StructType.js'
import { KotlinCxxBridgedType } from './KotlinCxxBridgedType.js'
export function createKotlinStruct(structType: StructType): SourceFile[] {
const packageName = NitroConfig.getAndroidPackage('java/kotlin')
const values = structType.properties.map(
(p) => `val ${p.escapedName}: ${p.getCode('kotlin')}`
)
let secondaryConstructor: string
const needsSpecialHandling = structType.properties.some(
(p) => new KotlinCxxBridgedType(p).needsSpecialHandling
)
if (needsSpecialHandling) {
const params = structType.properties.map((p) => {
const bridged = new KotlinCxxBridgedType(p)
return `${p.escapedName}: ${bridged.getTypeCode('kotlin')}`
})
const paramsForward = structType.properties.map((p) => {
const bridged = new KotlinCxxBridgedType(p)
if (bridged.needsSpecialHandling) {
// We need special parsing for this type
const parsed = bridged.parseFromCppToKotlin(
p.escapedName,
'kotlin',
false
)
// we explicitly `as`-cast this to avoid ambiguous upcasts/cyclic this() calls.
return `${parsed} as ${p.getCode('kotlin')}`
} else {
return p.escapedName
}
})
secondaryConstructor = `
@DoNotStrip
@Keep
@Suppress("unused")
private constructor(${indent(params.join(', '), 20)})
: this(${indent(paramsForward.join(', '), 20)})
`.trim()
} else {
secondaryConstructor = `/* main constructor */`
}
const code = `
${createFileMetadataString(`${structType.structName}.kt`)}
package ${packageName}
import androidx.annotation.Keep
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.core.*
/**
* Represents the JavaScript object/struct "${structType.structName}".
*/
@DoNotStrip
@Keep
data class ${structType.structName}
@DoNotStrip
@Keep
constructor(
${indent(values.join(',\n'), ' ')}
) {
${indent(secondaryConstructor, ' ')}
}
`.trim()
const cxxNamespace = NitroConfig.getCxxNamespace('c++')
const jniClassDescriptor = NitroConfig.getAndroidPackage(
'c++/jni',
structType.structName
)
const jniStructInitializerBody = createJNIStructInitializer(structType)
const cppStructInitializerBody = createCppStructInitializer(
'value',
structType
)
const imports = structType.properties
.flatMap((p) => getReferencedTypes(p))
.map((t) => new KotlinCxxBridgedType(t))
.flatMap((t) => t.getRequiredImports())
const includes = imports
.map((i) => includeHeader(i))
.filter(isNotDuplicate)
.sort()
const fbjniCode = `
${createFileMetadataString(`J${structType.structName}.hpp`)}
#pragma once
#include <fbjni/fbjni.h>
#include "${structType.declarationFile.name}"
${includes.join('\n')}
namespace ${cxxNamespace} {
using namespace facebook;
/**
* The C++ JNI bridge between the C++ struct "${structType.structName}" and the the Kotlin data class "${structType.structName}".
*/
struct J${structType.structName} final: public jni::JavaClass<J${structType.structName}> {
public:
static auto constexpr kJavaDescriptor = "L${jniClassDescriptor};";
public:
/**
* Convert this Java/Kotlin-based struct to the C++ struct ${structType.structName} by copying all values to C++.
*/
[[maybe_unused]]
[[nodiscard]]
${structType.structName} toCpp() const {
${indent(jniStructInitializerBody, ' ')}
}
public:
/**
* Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
*/
[[maybe_unused]]
static jni::local_ref<J${structType.structName}::javaobject> fromCpp(const ${structType.structName}& value) {
${indent(cppStructInitializerBody, ' ')}
}
};
} // namespace ${cxxNamespace}
`.trim()
const files: SourceFile[] = []
files.push({
content: code,
language: 'kotlin',
name: `${structType.structName}.kt`,
subdirectory: NitroConfig.getAndroidPackageDirectory(),
platform: 'android',
})
files.push({
content: fbjniCode,
language: 'c++',
name: `J${structType.structName}.hpp`,
subdirectory: [],
platform: 'android',
})
return files
}
function createJNIStructInitializer(structType: StructType): string {
const lines: string[] = ['static const auto clazz = javaClassStatic();']
for (const prop of structType.properties) {
const fieldName = `field${capitalizeName(prop.escapedName)}`
const jniType = new KotlinCxxBridgedType(prop)
const signatureType = jniType.getTypeCode('c++')
const valueType = jniType.asJniReferenceType('local')
lines.push(
`static const auto ${fieldName} = clazz->getField<${signatureType}>("${prop.escapedName}");`
)
lines.push(
`${valueType} ${prop.escapedName} = this->getFieldValue(${fieldName});`
)
}
const propsForward = structType.properties.map((p) => {
const bridged = new KotlinCxxBridgedType(p)
return bridged.parse(p.escapedName, 'kotlin', 'c++', 'c++')
})
lines.push(`return ${structType.structName}(`)
lines.push(` ${indent(propsForward.join(',\n'), ' ')}`)
lines.push(`);`)
return lines.join('\n')
}
function createCppStructInitializer(
cppValueName: string,
structType: StructType
): string {
const lines: string[] = []
lines.push(`return newInstance(`)
const names = structType.properties.map((p) => {
const name = `${cppValueName}.${p.escapedName}`
const bridge = new KotlinCxxBridgedType(p)
return bridge.parse(name, 'c++', 'kotlin', 'c++')
})
lines.push(` ${indent(names.join(',\n'), ' ')}`)
lines.push(');')
return lines.join('\n')
}