blob: 96abd233fb6f1e1a4195a6735d79e52b9cbc7304 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:ffigen/src/code_generator.dart';
import 'ffigen_util.dart';
import 'logging.dart';
class Paths {
static final currentDir = Directory.current.uri;
static final src = currentDir.resolve("src/");
static final thirdParty = src.resolve("third_party/");
static final globalJniEnvH = thirdParty.resolve("global_jni_env.h");
static final globalJniEnvC = thirdParty.resolve("global_jni_env.c");
static final bindingsDir = currentDir.resolve("lib/src/third_party/");
static final envExtensions = bindingsDir.resolve("env_extensions.dart");
}
/// Name of variable used in wrappers to hold the result.
const resultVar = '_result';
/// Name of variable used in wrappers to hold the exception.
const errorVar = '_exception';
/// Name of JNIEnv struct definition in JNI headers.
const envType = 'JNINativeInterface';
/// Name of wrapper to JNIEnv
const wrapperName = 'GlobalJniEnvStruct';
const wrapperIncludes = '''
#include "global_jni_env.h"
''';
const wrapperDeclIncludes = '''
#include <stdint.h>
#include "../dartjni.h"
''';
const wrapperGetter = '''
FFI_PLUGIN_EXPORT
$wrapperName* GetGlobalEnv() {
if (jni->jvm == NULL) {
return NULL;
}
return &globalJniEnv;
}
''';
const wrapperGetterDecl = '''
FFI_PLUGIN_EXPORT $wrapperName* GetGlobalEnv();
''';
bool hasVarArgs(String name) {
return name == 'NewObject' ||
RegExp(r'^Call(Static|Nonvirtual|)[A-Z][a-z]+Method$').hasMatch(name);
}
/// Get C name of a type from its ffigen representation.
String getCType(Type type) {
if (type is PointerType) {
return '${getCType(type.child)}*';
}
final cType = type.getCType(dummyWriter);
const specialCaseMappings = {
'JNIEnv1': 'JNIEnv',
'ffi.Char': 'char',
'ffi.Void': 'void',
'ffi.Int': 'int',
'ffi.Int32': 'int32_t',
};
return specialCaseMappings[cType] ?? cType;
}
/// Get type of wrapping function for a JNIEnv function.
FunctionType getGlobalJniEnvFunctionType(FunctionType ft) {
return FunctionType(
returnType: ft.returnType,
parameters: ft.parameters.sublist(1),
);
}
// Returns declaration of function field in GlobalJniEnv struct
String getFunctionFieldDecl(Member field, {required bool isField}) {
final fieldType = field.type;
if (fieldType is PointerType && fieldType.child is NativeFunc) {
final nativeFunc = fieldType.child as NativeFunc;
final functionType = getGlobalJniEnvFunctionType(nativeFunc.type);
final resultWrapper = getResultWrapper(getCType(functionType.returnType));
final name = field.name;
final withVarArgs = hasVarArgs(name);
final params = functionType.parameters
.map((param) => '${getCType(param.type)} ${param.name}')
.join(', ') +
(withVarArgs ? ', ...' : '');
final willExport = withVarArgs ? 'FFI_PLUGIN_EXPORT ' : '';
if (isField) {
return '${resultWrapper.returnType} (*$name)($params);';
}
return '$willExport${resultWrapper.returnType} '
'${getWrapperFuncName(field)}($params);';
} else {
return 'void* ${field.name};';
}
}
String getWrapperFuncName(Member field) {
return 'globalEnv_${field.name}';
}
class ResultWrapper {
String returnType, onResult, onError;
ResultWrapper.withResultAndError(
this.returnType, this.onResult, this.onError);
ResultWrapper.unionType(
String returnType,
String defaultValue,
) : this.withResultAndError(
returnType,
'($returnType){.value = $resultVar, .exception = NULL}',
'($returnType){.value = $defaultValue, .exception = $errorVar}',
);
ResultWrapper.forJValueField(String fieldChar)
: this.withResultAndError(
'JniResult',
'(JniResult){.value = {.$fieldChar = $resultVar}, .exception = NULL}',
'(JniResult){.value = {.j = 0}, .exception = $errorVar}',
);
}
ResultWrapper getResultWrapper(String returnType) {
if (returnType.endsWith("*")) {
return ResultWrapper.unionType('JniPointerResult', 'NULL');
}
final jobjectWrapper = ResultWrapper.forJValueField('l');
if (returnType.endsWith('Array')) {
return jobjectWrapper;
}
const jfields = {
'jboolean': 'z',
'jbyte': 'b',
'jshort': 's',
'jchar': 'c',
'jint': 'i',
'jsize': 'i', // jsize is an alias to jint
'jfloat': 'f',
'jlong': 'j',
'jdouble': 'd',
'jobject': 'l',
'jweak': 'l',
'jarray': 'l',
'jstring': 'l',
'jthrowable': 'l',
};
switch (returnType) {
case 'void':
return ResultWrapper.withResultAndError(
'jthrowable',
'NULL',
errorVar,
);
case 'jmethodID':
case 'jfieldID':
return ResultWrapper.unionType('JniPointerResult', 'NULL');
case 'jclass':
return ResultWrapper.unionType('JniClassLookupResult', 'NULL');
case 'int32_t':
return ResultWrapper.forJValueField('i');
default:
if (jfields.containsKey(returnType)) {
return ResultWrapper.forJValueField(jfields[returnType]!);
}
throw 'Unknown type $returnType for return type';
}
}
bool isJRefType(String type) {
// No need to include jweak here, its only returned by ref-related functions.
const refTypes = {
'jclass',
'jobject',
'jstring',
'jthrowable',
'jarray',
'jweak'
};
return (type.startsWith('j') && type.endsWith('Array')) ||
refTypes.contains(type);
}
const refFunctions = {
'NewGlobalRef',
'DeleteGlobalRef',
'NewLocalRef',
'DeleteLocalRef',
'NewWeakGlobalRef',
'DeleteWeakGlobalRef',
};
/// These return const ptrs so the assignment statement needs to be
/// adjusted in the wrapper.
const constBufferReturningFunctions = {
'GetStringChars',
'GetStringUTFChars',
'GetStringCritical',
};
/// Methods which do not throw exceptions, and thus not need to be checked
const _noCheckException = {
'GetVersion',
'GetStringCritical',
'ExceptionClear',
'ExceptionDescribe',
};
String? getWrapperFunc(Member field) {
final fieldType = field.type;
if (fieldType is PointerType && fieldType.child is NativeFunc) {
final functionType = (fieldType.child as NativeFunc).type;
if (functionType.parameters.first.name.isEmpty) {
return null;
}
final outerFunctionType = getGlobalJniEnvFunctionType(functionType);
final wrapperName = getWrapperFuncName(field);
final returnType = getCType(outerFunctionType.returnType);
final withVarArgs = hasVarArgs(field.name);
final params = [
...outerFunctionType.parameters
.map((param) => '${getCType(param.type)} ${param.name}'),
if (withVarArgs) '...',
].join(', ');
var returnCapture = returnType == 'void' ? '' : '$returnType $resultVar =';
if (constBufferReturningFunctions.contains(field.name)) {
returnCapture = 'const $returnCapture';
}
final callParams = [
'jniEnv',
...(outerFunctionType.parameters.map((param) => param.name).toList()),
if (withVarArgs) 'args',
].join(', ');
final resultWrapper = getResultWrapper(returnType);
var convertRef = '';
if (isJRefType(returnType) && !refFunctions.contains(field.name)) {
convertRef = ' $resultVar = to_global_ref($resultVar);\n';
}
final callee = field.name + (withVarArgs ? 'V' : '');
final varArgsInit = withVarArgs
? '''
va_list args;
va_start(args, methodID);
'''
: '';
final varArgsEnd = withVarArgs ? 'va_end(args);\n' : '';
final exceptionCheck = _noCheckException.contains(field.name)
? ''
: '''
jthrowable $errorVar = check_exception();
if ($errorVar != NULL) {
return ${resultWrapper.onError};
}
''';
final willExport = withVarArgs ? 'FFI_PLUGIN_EXPORT ' : '';
return '$willExport'
'${resultWrapper.returnType} $wrapperName($params) {\n'
' attach_thread();\n'
'$varArgsInit'
' $returnCapture (*jniEnv)->$callee($callParams);\n'
'$varArgsEnd'
'$exceptionCheck'
'$convertRef'
' return ${resultWrapper.onResult};\n'
'}\n';
}
return null;
}
void writeGlobalJniEnvWrapper(Library library) {
final jniEnvType = findCompound(library, envType);
final fieldDecls = jniEnvType.members
.map((member) => getFunctionFieldDecl(member, isField: true))
.join('\n');
final varArgsFunctions = jniEnvType.members
.where((member) => hasVarArgs(member.name))
.map((member) => getFunctionFieldDecl(member, isField: false))
.join('\n');
final structDecl =
'typedef struct $wrapperName {\n$fieldDecls\n} $wrapperName;\n';
File.fromUri(Paths.globalJniEnvH).writeAsStringSync('$preamble'
'$wrapperDeclIncludes'
'$structDecl'
'$wrapperGetterDecl'
'$varArgsFunctions\n');
final functionWrappers = StringBuffer();
final structInst = StringBuffer('$wrapperName globalJniEnv = {\n');
for (final member in jniEnvType.members) {
final wrapper = getWrapperFunc(member);
if (wrapper == null) {
structInst.write('.${member.name} = NULL,\n');
} else {
structInst.write('.${member.name} = ${getWrapperFuncName(member)},\n');
functionWrappers.write('$wrapper\n');
}
}
structInst.write('};');
File.fromUri(Paths.globalJniEnvC).writeAsStringSync(
'$preamble$wrapperIncludes$functionWrappers$structInst$wrapperGetter');
}
void executeClangFormat(List<Uri> files) {
final paths = files.map((u) => u.toFilePath()).toList();
logger.info('execute clang-format -i ${paths.join(" ")}');
final format = Process.runSync('clang-format', ['-i', ...paths]);
if (format.exitCode != 0) {
stderr.writeln('clang-format exited with ${format.exitCode}');
stderr.writeln(format.stderr);
}
}
void generateCWrappers(Library minimalLibrary) {
writeGlobalJniEnvWrapper(minimalLibrary);
executeClangFormat([Paths.globalJniEnvC, Paths.globalJniEnvH]);
}