| // 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 '../config/config.dart'; |
| import '../elements/elements.dart'; |
| import '../logging/logging.dart'; |
| import '../util/find_package.dart'; |
| import '../util/string_util.dart'; |
| import 'visitor.dart'; |
| |
| class CFieldName extends Visitor<Field, String> { |
| const CFieldName(); |
| |
| @override |
| String visit(Field node) { |
| final className = node.classDecl.uniqueName; |
| final fieldName = node.finalName; |
| return '${className}__$fieldName'; |
| } |
| } |
| |
| class CMethodName extends Visitor<Method, String> { |
| const CMethodName(); |
| |
| @override |
| String visit(Method node) { |
| final className = node.classDecl.uniqueName; |
| final methodName = node.finalName; |
| return '${className}__$methodName'; |
| } |
| } |
| |
| class CGenerator extends Visitor<Classes, Future<void>> { |
| static const _prelude = '''// Autogenerated by jnigen. DO NOT EDIT! |
| |
| #include <stdint.h> |
| #include "jni.h" |
| #include "dartjni.h" |
| |
| thread_local JNIEnv *jniEnv; |
| JniContext *jni; |
| |
| JniContext *(*context_getter)(void); |
| JNIEnv *(*env_getter)(void); |
| |
| void setJniGetters(JniContext *(*cg)(void), |
| JNIEnv *(*eg)(void)) { |
| context_getter = cg; |
| env_getter = eg; |
| } |
| |
| '''; |
| |
| final Config config; |
| |
| CGenerator(this.config); |
| |
| Future<void> _copyFileFromPackage(String package, String relPath, Uri target, |
| {String Function(String)? transform}) async { |
| final packagePath = await findPackageRoot(package); |
| if (packagePath != null) { |
| final sourceFile = File.fromUri(packagePath.resolve(relPath)); |
| final targetFile = await File.fromUri(target).create(recursive: true); |
| var source = await sourceFile.readAsString(); |
| if (transform != null) { |
| source = transform(source); |
| } |
| await targetFile.writeAsString(source); |
| } else { |
| log.warning('package $package not found! ' |
| 'skipped copying ${target.toFilePath()}'); |
| } |
| } |
| |
| @override |
| Future<void> visit(Classes node) async { |
| // Write C file and init file. |
| final cConfig = config.outputConfig.cConfig!; |
| final cRoot = cConfig.path; |
| final preamble = config.preamble; |
| log.info("Using c root = $cRoot"); |
| final libraryName = cConfig.libraryName; |
| log.info('Creating dart init file ...'); |
| // Create C file. |
| final subdir = cConfig.subdir ?? '.'; |
| final cFileRelativePath = '$subdir/$libraryName.c'; |
| final cFile = await File.fromUri(cRoot.resolve(cFileRelativePath)) |
| .create(recursive: true); |
| final cFileStream = cFile.openWrite(); |
| // Write C Bindings. |
| if (preamble != null) { |
| cFileStream.writeln(preamble); |
| } |
| cFileStream.write(_prelude); |
| final classGenerator = _CClassGenerator(config, cFileStream); |
| for (final classDecl in node.decls.values) { |
| classDecl.accept(classGenerator); |
| } |
| await cFileStream.close(); |
| log.info('Copying auxiliary files...'); |
| for (final file in ['dartjni.h', '.clang-format']) { |
| await _copyFileFromPackage( |
| 'jni', 'src/$file', cRoot.resolve('$subdir/$file')); |
| } |
| await _copyFileFromPackage( |
| 'jnigen', 'cmake/CMakeLists.txt.tmpl', cRoot.resolve('CMakeLists.txt'), |
| transform: (s) { |
| return s |
| .replaceAll('{{LIBRARY_NAME}}', libraryName) |
| .replaceAll('{{SUBDIR}}', subdir); |
| }); |
| log.info('Running clang-format on C bindings'); |
| try { |
| final clangFormat = Process.runSync('clang-format', ['-i', cFile.path]); |
| if (clangFormat.exitCode != 0) { |
| printError(clangFormat.stderr); |
| log.warning('clang-format exited with ${clangFormat.exitCode}'); |
| } |
| } on ProcessException catch (e) { |
| log.warning('cannot run clang-format: $e'); |
| } |
| } |
| } |
| |
| const _classVarPrefix = '_c_'; |
| const _jniResultType = 'JniResult'; |
| const _loadEnvCall = 'load_env();'; |
| const _ifError = |
| '(JniResult){.value = {.j = 0}, .exception = check_exception()}'; |
| |
| class _CClassGenerator extends Visitor<ClassDecl, void> { |
| final Config config; |
| final StringSink s; |
| |
| _CClassGenerator(this.config, this.s); |
| |
| @override |
| void visit(ClassDecl node) { |
| final classNameInC = node.uniqueName; |
| final classVar = '$_classVarPrefix$classNameInC'; |
| // Global variable in C that holds the reference to class. |
| s.write('''// ${node.binaryName} |
| jclass $classVar = NULL; |
| |
| '''); |
| |
| final methodGenerator = _CMethodGenerator(config, s); |
| for (final method in node.methods) { |
| method.accept(methodGenerator); |
| } |
| |
| final fieldGenerator = _CFieldGenerator(config, s); |
| for (final field in node.fields) { |
| field.accept(fieldGenerator); |
| } |
| } |
| } |
| |
| class _CLoadClassGenerator extends Visitor<ClassDecl, String> { |
| _CLoadClassGenerator(); |
| |
| @override |
| String visit(ClassDecl node) { |
| final classVar = '$_classVarPrefix${node.uniqueName}'; |
| return ''' load_class_global_ref(&$classVar, "${node.internalName}"); |
| if ($classVar == NULL) return $_ifError;'''; |
| } |
| } |
| |
| class _CMethodGenerator extends Visitor<Method, void> { |
| static const _methodVarPrefix = '_m_'; |
| |
| final Config config; |
| final StringSink s; |
| |
| _CMethodGenerator(this.config, this.s); |
| |
| @override |
| void visit(Method node) { |
| final classNameInC = node.classDecl.uniqueName; |
| |
| final cMethodName = node.accept(const CMethodName()); |
| final classRef = '$_classVarPrefix$classNameInC'; |
| final methodId = '$_methodVarPrefix$cMethodName'; |
| final cMethodParams = [ |
| if (!node.isCtor && !node.isStatic) 'jobject self_', |
| ...node.params.accept(const _CParamGenerator(addReturnType: true)), |
| ].join(','); |
| final jniSignature = node.descriptor; |
| final ifStaticMethodID = node.isStatic ? 'static_' : ''; |
| |
| var javaReturnType = node.returnType.type; |
| if (node.isCtor) { |
| javaReturnType = DeclaredType( |
| binaryName: node.classDecl.binaryName, |
| ); |
| } |
| final callType = node.returnType.accept(const _CTypeCallSite()); |
| final callArgs = [ |
| 'jniEnv', |
| if (!node.isCtor && !node.isStatic) 'self_' else classRef, |
| methodId, |
| ...node.params.accept(const _CParamGenerator(addReturnType: false)) |
| ].join(', '); |
| |
| var ifAssignResult = ''; |
| if (javaReturnType.name != 'void') { |
| ifAssignResult = |
| '${javaReturnType.accept(const _CReturnType())} _result = '; |
| } |
| |
| final ifStaticCall = node.isStatic ? 'Static' : ''; |
| final envMethod = |
| node.isCtor ? 'NewObject' : 'Call$ifStaticCall${callType}Method'; |
| final returnResultIfAny = javaReturnType.accept(const _CResult()); |
| s.write(''' |
| jmethodID $methodId = NULL; |
| FFI_PLUGIN_EXPORT |
| $_jniResultType $cMethodName($cMethodParams) { |
| $_loadEnvCall |
| ${node.classDecl.accept(_CLoadClassGenerator())} |
| load_${ifStaticMethodID}method($classRef, |
| &$methodId, "${node.name}", "$jniSignature"); |
| if ($methodId == NULL) return $_ifError; |
| $ifAssignResult(*jniEnv)->$envMethod($callArgs); |
| $returnResultIfAny |
| } |
| |
| '''); |
| } |
| } |
| |
| class _CFieldGenerator extends Visitor<Field, void> { |
| static const _fieldVarPrefix = '_f_'; |
| |
| final Config config; |
| final StringSink s; |
| |
| _CFieldGenerator(this.config, this.s); |
| |
| @override |
| void visit(Field node) { |
| final cClassName = node.classDecl.uniqueName; |
| |
| final fieldName = node.finalName; |
| final fieldNameInC = node.accept(const CFieldName()); |
| final fieldVar = "$_fieldVarPrefix$fieldNameInC"; |
| |
| // If the field is final and default is assigned, then no need to wrap |
| // this field. It should then be a constant in dart code. |
| if (node.isStatic && |
| node.isFinal && |
| node.defaultValue != null && |
| (node.defaultValue is num || node.defaultValue is bool)) { |
| return; |
| } |
| |
| s.write('jfieldID $fieldVar = NULL;\n'); |
| |
| final classVar = '$_classVarPrefix$cClassName'; |
| void writeAccessor({bool isSetter = false}) { |
| const cReturnType = _jniResultType; |
| final cMethodPrefix = isSetter ? 'set' : 'get'; |
| final formalArgs = [ |
| if (!node.isStatic) 'jobject self_', |
| if (isSetter) '${node.type.accept(const _CReturnType())} value', |
| ].join(', '); |
| final ifStaticField = node.isStatic ? 'static_' : ''; |
| final ifStaticCall = node.isStatic ? 'Static' : ''; |
| final callType = node.type.accept(const _CTypeCallSite()); |
| final objectArgument = node.isStatic ? classVar : 'self_'; |
| |
| String accessorStatements; |
| if (isSetter) { |
| accessorStatements = |
| ' (*jniEnv)->Set$ifStaticCall${callType}Field(jniEnv, ' |
| '$objectArgument, $fieldVar, value);\n' |
| ' return $_ifError;'; |
| } else { |
| final getterExpr = |
| '(*jniEnv)->Get$ifStaticCall${callType}Field(jniEnv, ' |
| '$objectArgument, $fieldVar)'; |
| final cResultType = node.type.accept(const _CReturnType()); |
| final result = node.type.accept(const _CResult()); |
| accessorStatements = ''' $cResultType _result = $getterExpr; |
| $result'''; |
| } |
| s.write(''' |
| FFI_PLUGIN_EXPORT |
| $cReturnType ${cMethodPrefix}_$fieldNameInC($formalArgs) { |
| $_loadEnvCall |
| ${node.classDecl.accept(_CLoadClassGenerator())} |
| load_${ifStaticField}field($classVar, &$fieldVar, "$fieldName", |
| "${node.type.descriptor}"); |
| $accessorStatements |
| } |
| |
| '''); |
| } |
| |
| writeAccessor(isSetter: false); |
| if (node.isFinal) { |
| return; |
| } |
| writeAccessor(isSetter: true); |
| } |
| } |
| |
| class _CParamGenerator extends Visitor<Param, String> { |
| /// These should be avoided in parameter names. |
| static const _cTypeKeywords = { |
| 'short', |
| 'char', |
| 'int', |
| 'long', |
| 'float', |
| 'double', |
| }; |
| |
| const _CParamGenerator({required this.addReturnType}); |
| |
| final bool addReturnType; |
| |
| @override |
| String visit(Param node) { |
| final paramName = |
| (_cTypeKeywords.contains(node.name) ? '${node.name}0' : node.name) |
| .replaceAll('\$', '_'); |
| if (addReturnType) { |
| final type = node.type.accept(const _CReturnType()); |
| return '$type $paramName'; |
| } |
| return paramName; |
| } |
| } |
| |
| class _CReturnType extends TypeVisitor<String> { |
| const _CReturnType(); |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return 'jobject'; |
| } |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| return node.cType; |
| } |
| } |
| |
| class _CTypeCallSite extends TypeVisitor<String> { |
| const _CTypeCallSite(); |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return 'Object'; |
| } |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| return node.name.capitalize(); |
| } |
| } |
| |
| class _CResult extends TypeVisitor<String> { |
| const _CResult(); |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return 'return to_global_ref_result(_result);'; |
| } |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| if (node.name == 'void') { |
| return 'return $_ifError;'; |
| } |
| // The union field is the same as the type's signature, but in lowercase. |
| final unionField = node.signature.toLowerCase(); |
| return 'return (JniResult){.value = {.$unionField = _result}, ' |
| '.exception = check_exception()};'; |
| } |
| } |