nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
270 lines (237 loc) • 8.37 kB
text/typescript
import { NitroConfig } from '../../config/NitroConfig.js'
import { indent } from '../../utils.js'
import { includeHeader } from '../c++/includeNitroHeader.js'
import { createFileMetadataString, isNotDuplicate } from '../helpers.js'
import type { SourceFile } from '../SourceFile.js'
import type { FunctionType } from '../types/FunctionType.js'
import { addJNINativeRegistration } from './JNINativeRegistrations.js'
import { KotlinCxxBridgedType } from './KotlinCxxBridgedType.js'
export function createKotlinFunction(functionType: FunctionType): SourceFile[] {
const name = functionType.specializationName
const packageName = NitroConfig.getAndroidPackage('java/kotlin')
const kotlinReturnType = functionType.returnType.getCode('kotlin')
const kotlinParams = functionType.parameters.map(
(p) => `${p.escapedName}: ${p.getCode('kotlin')}`
)
const kotlinParamTypes = functionType.parameters.map((p) =>
p.getCode('kotlin')
)
const kotlinParamsForward = functionType.parameters.map((p) => p.escapedName)
const lambdaSignature = `(${kotlinParamTypes.join(', ')}) -> ${kotlinReturnType}`
const kotlinCode = `
${createFileMetadataString(`${name}.kt`)}
package ${packageName}
import androidx.annotation.Keep
import com.facebook.jni.HybridData
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.core.*
import dalvik.annotation.optimization.FastNative
/**
* Represents the JavaScript callback \`${functionType.jsName}\`.
* This can be either implemented in C++ (in which case it might be a callback coming from JS),
* or in Kotlin/Java (in which case it is a native callback).
*/
@DoNotStrip
@Keep
@Suppress("ClassName", "RedundantUnitReturnType")
fun interface ${name}: ${lambdaSignature} {
/**
* Call the given JS callback.
* @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted.
*/
@DoNotStrip
@Keep
override fun invoke(${kotlinParams.join(', ')}): ${kotlinReturnType}
}
/**
* Represents the JavaScript callback \`${functionType.jsName}\`.
* This is implemented in C++, via a \`std::function<...>\`.
* The callback might be coming from JS.
*/
@DoNotStrip
@Keep
@Suppress(
"KotlinJniMissingFunction", "unused",
"RedundantSuppression", "RedundantUnitReturnType", "FunctionName",
"ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName",
)
class ${name}_cxx: ${name} {
@DoNotStrip
@Keep
private val mHybridData: HybridData
@DoNotStrip
@Keep
private constructor(hybridData: HybridData) {
mHybridData = hybridData
}
@DoNotStrip
@Keep
override fun invoke(${kotlinParams.join(', ')}): ${kotlinReturnType}
= invoke_cxx(${kotlinParamsForward.join(',')})
@FastNative
private external fun invoke_cxx(${kotlinParams.join(', ')}): ${kotlinReturnType}
}
/**
* Represents the JavaScript callback \`${functionType.jsName}\`.
* This is implemented in Java/Kotlin, via a \`${lambdaSignature}\`.
* The callback is always coming from native.
*/
@DoNotStrip
@Keep
@Suppress("ClassName", "RedundantUnitReturnType", "unused")
class ${name}_java(private val function: ${lambdaSignature}): ${name} {
@DoNotStrip
@Keep
override fun invoke(${kotlinParams.join(', ')}): ${kotlinReturnType} {
return this.function(${kotlinParamsForward.join(', ')})
}
}
`.trim()
const jniInterfaceDescriptor = NitroConfig.getAndroidPackage('c++/jni', name)
const jniClassDescriptor = NitroConfig.getAndroidPackage(
'c++/jni',
`${name}_cxx`
)
const bridgedReturn = new KotlinCxxBridgedType(functionType.returnType)
const cxxNamespace = NitroConfig.getCxxNamespace('c++')
const typename = functionType.getCode('c++')
// call() Java -> C++
const cppParams = functionType.parameters.map((p) => {
const bridge = new KotlinCxxBridgedType(p)
const type = bridge.asJniReferenceType('alias')
return `${type} ${p.escapedName}`
})
const paramsForward = functionType.parameters.map((p) => {
const bridge = new KotlinCxxBridgedType(p)
return bridge.parseFromKotlinToCpp(p.escapedName, 'c++', false)
})
// call() C++ -> Java
const jniParams = functionType.parameters.map((p) => {
if (p.canBePassedByReference) {
return `const ${p.getCode('c++')}& ${p.escapedName}`
} else {
return `${p.getCode('c++')} ${p.escapedName}`
}
})
const jniParamsForward = [
'self()',
...functionType.parameters.map((p) => {
const bridge = new KotlinCxxBridgedType(p)
return bridge.parseFromCppToKotlin(p.escapedName, 'c++', false)
}),
]
const jniSignature = `${bridgedReturn.asJniReferenceType('local')}(${functionType.parameters
.map((p) => {
const bridge = new KotlinCxxBridgedType(p)
return `${bridge.asJniReferenceType('alias')} /* ${p.escapedName} */`
})
.join(', ')})`
let cppCallBody: string
let jniCallBody: string
if (functionType.returnType.kind === 'void') {
// It returns void
cppCallBody = `_func(${indent(paramsForward.join(', '), ' ')});`
jniCallBody = `
static const auto method = javaClassStatic()->getMethod<${jniSignature}>("invoke");
method(${jniParamsForward.join(', ')});
`.trim()
} else {
// It returns a type!
cppCallBody = `
${functionType.returnType.getCode('c++')} __result = _func(${indent(paramsForward.join(', '), ' ')});
return ${bridgedReturn.parseFromCppToKotlin('__result', 'c++')};
`.trim()
jniCallBody = `
static const auto method = javaClassStatic()->getMethod<${jniSignature}>("invoke");
auto __result = method(${jniParamsForward.join(', ')});
return ${bridgedReturn.parseFromKotlinToCpp('__result', 'c++', false)};
`.trim()
}
const bridged = new KotlinCxxBridgedType(functionType)
const imports = bridged
.getRequiredImports()
.filter((i) => i.name !== `J${name}.hpp`)
const includes = imports.map((i) => includeHeader(i)).filter(isNotDuplicate)
const fbjniCode = `
${createFileMetadataString(`J${name}.hpp`)}
#pragma once
#include <fbjni/fbjni.h>
#include <functional>
${includes.join('\n')}
namespace ${cxxNamespace} {
using namespace facebook;
/**
* Represents the Java/Kotlin callback \`${functionType.getCode('kotlin')}\`.
* This can be passed around between C++ and Java/Kotlin.
*/
struct J${name}: public jni::JavaClass<J${name}> {
public:
static auto constexpr kJavaDescriptor = "L${jniInterfaceDescriptor};";
public:
/**
* Invokes the function this \`J${name}\` instance holds through JNI.
*/
${functionType.returnType.getCode('c++')} invoke(${jniParams.join(', ')}) const {
${indent(jniCallBody, ' ')}
}
};
/**
* An implementation of ${name} that is backed by a C++ implementation (using \`std::function<...>\`)
*/
struct J${name}_cxx final: public jni::HybridClass<J${name}_cxx, J${name}> {
public:
static jni::local_ref<J${name}::javaobject> fromCpp(const ${typename}& func) {
return J${name}_cxx::newObjectCxxArgs(func);
}
public:
/**
* Invokes the C++ \`std::function<...>\` this \`J${name}_cxx\` instance holds.
*/
${bridgedReturn.asJniReferenceType('local')} invoke_cxx(${cppParams.join(', ')}) {
${indent(cppCallBody, ' ')}
}
public:
[[nodiscard]]
inline const ${typename}& getFunction() const {
return _func;
}
public:
static auto constexpr kJavaDescriptor = "L${jniClassDescriptor};";
static void registerNatives() {
registerHybrid({makeNativeMethod("invoke_cxx", J${name}_cxx::invoke_cxx)});
}
private:
explicit J${name}_cxx(const ${typename}& func): _func(func) { }
private:
friend HybridBase;
${typename} _func;
};
} // namespace ${cxxNamespace}
`.trim()
// Make sure we register all native JNI methods on app startup
addJNINativeRegistration({
namespace: cxxNamespace,
className: `J${name}_cxx`,
import: {
name: `J${name}.hpp`,
space: 'user',
language: 'c++',
},
})
const files: SourceFile[] = []
files.push({
content: kotlinCode,
language: 'kotlin',
name: `${name}.kt`,
subdirectory: NitroConfig.getAndroidPackageDirectory(),
platform: 'android',
})
files.push({
content: fbjniCode,
language: 'c++',
name: `J${name}.hpp`,
subdirectory: [],
platform: 'android',
})
return files
}