blob: d26d9c496768e1d0544cb1937f1eaf3c4c9ed92f [file]
// Copyright (c) 2022, 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 'dart:math';
import 'package:jnigen/src/bindings/bindings.dart';
import 'package:jnigen/src/logging/logging.dart';
import 'package:jnigen/src/elements/elements.dart';
import 'package:jnigen/src/config/config.dart';
import 'package:jnigen/src/util/find_package.dart';
import 'package:jnigen/src/util/name_utils.dart';
import 'package:jnigen/src/writers/bindings_writer.dart';
/// Resolver for file-per-package mapping, in which the java package hierarchy
/// is mirrored.
class PackagePathResolver implements SymbolResolver {
PackagePathResolver(this.importMap, this.currentPackage, this.inputClassNames,
{this.predefined = const {}});
final String currentPackage;
final Map<String, String> importMap;
final Map<String, String> predefined;
final Set<String> inputClassNames;
final List<String> importStrings = [];
final Set<String> relativeImportedPackages = {};
final Map<String, String> _importedNameToPackage = {};
final Map<String, String> _packageToImportedName = {};
/// Returns the dart name of the [binaryName] in current translation context,
/// or `null` if the name cannot be resolved.
@override
String? resolve(String binaryName) {
if (predefined.containsKey(binaryName)) {
return predefined[binaryName];
}
final parts = cutFromLast(binaryName, '.');
final package = parts[0];
final typename = parts[1];
final simpleTypeName = typename.replaceAll('\$', '_');
if (package == currentPackage && inputClassNames.contains(binaryName)) {
return simpleTypeName;
}
if (_packageToImportedName.containsKey(package)) {
// This package was already resolved
// but we still need to check if it was a relative import, in which case
// the class not in inputClassNames cannot be mapped here.
if (!relativeImportedPackages.contains(package) ||
inputClassNames.contains(binaryName)) {
final importedName = _packageToImportedName[package];
return '$importedName.$simpleTypeName';
}
}
final packageImport = getImport(package, binaryName);
log.finest('$package resolved to $packageImport for $binaryName');
if (packageImport == null) {
return null;
}
final pkgName = cutFromLast(package, '.')[1];
if (pkgName.isEmpty) {
throw UnsupportedError('No package could be deduced from '
'qualified binaryName');
}
// We always name imports with an underscore suffix, so that they can be
// never shadowed by a parameter or local variable.
var importedName = '${pkgName}_';
int suffix = 0;
while (_importedNameToPackage.containsKey(importedName)) {
suffix++;
importedName = '$pkgName${suffix}_';
}
_importedNameToPackage[importedName] = package;
_packageToImportedName[package] = importedName;
importStrings.add('import "$packageImport" as $importedName;\n');
return '$importedName.$simpleTypeName';
}
/// Returns import string for [packageToResolve], or `null` if package not
/// found.
///
/// [binaryName] is the class name trying to be resolved. This parameter is
/// requested so that classes included in current bindings can be resolved
/// using relative path.
String? getImport(String packageToResolve, String binaryName) {
var prefix = packageToResolve;
// short circuit if the requested class is specified directly in import map.
if (importMap.containsKey(binaryName)) {
return importMap[binaryName]!;
}
if (prefix.isEmpty) {
throw UnsupportedError('unexpected: empty package name.');
}
final dest = packageToResolve.split('.');
final src = currentPackage.split('.');
// Use relative import when the required class is included in current set
// of bindings.
if (inputClassNames.contains(binaryName)) {
int common = 0;
// find the common prefix path directory of current package, and directory
// of target package
// src.length - 1 simply corresponds to directory of the package.
for (int i = 0; i < src.length - 1 && i < dest.length - 1; i++) {
if (src[i] == dest[i]) {
common++;
}
}
final pathToCommon = '../' * ((src.length - 1) - common);
final pathToPackage = dest.sublist(max(common, 0)).join('/');
relativeImportedPackages.add(packageToResolve);
return '$pathToCommon$pathToPackage.dart';
}
while (prefix.isNotEmpty) {
final split = cutFromLast(prefix, '.');
final left = split[0];
if (importMap.containsKey(prefix)) {
return importMap[prefix]!;
}
prefix = left;
}
return null;
}
@override
List<String> getImportStrings() {
return importStrings;
}
}
/// Writer which executes custom callback on passed class elements.
///
/// This class is provided for debugging purposes.
class CallbackWriter implements BindingsWriter {
CallbackWriter(this.callback);
Future<void> Function(Iterable<ClassDecl>) callback;
@override
Future<void> writeBindings(Iterable<ClassDecl> classes) async {
await callback(classes);
}
}
/// Writer which takes writes C and Dart bindings to specified directories.
///
/// The structure of dart files is determined by package structure of java.
/// One dart file corresponds to one java package, and it's path is decided by
/// fully qualified name of the package.
///
/// Example:
/// `android.os` -> `$dartWrappersRoot`/`android/os.dart`
class FilesWriter extends BindingsWriter {
static const _initFileName = '_init.dart';
FilesWriter(this.config);
Config config;
@override
Future<void> writeBindings(Iterable<ClassDecl> classes) async {
final preamble = config.preamble;
final Map<String, List<ClassDecl>> packages = {};
final Map<String, ClassDecl> classesByName = {};
for (var c in classes) {
classesByName.putIfAbsent(c.binaryName, () => c);
packages.putIfAbsent(c.packageName, () => <ClassDecl>[]);
packages[c.packageName]!.add(c);
}
final classNames = classesByName.keys.toSet();
final cRoot = config.outputConfig.cConfig.path;
log.info("Using c root = $cRoot");
final dartRoot = config.outputConfig.dartConfig.path;
log.info("Using dart root = $dartRoot");
final libraryName = config.outputConfig.cConfig.libraryName;
log.info('Creating dart init file ...');
final initFileUri = dartRoot.resolve(_initFileName);
final initFile = await File.fromUri(initFileUri).create(recursive: true);
var initCode = DartBindingsGenerator.initFile(libraryName);
if (preamble != null) {
initCode = '$preamble\n$initCode';
}
await initFile.writeAsString(initCode, flush: true);
final subdir = config.outputConfig.cConfig.subdir ?? '.';
final cFileRelativePath = '$subdir/$libraryName.c';
final cFile = await File.fromUri(cRoot.resolve(cFileRelativePath))
.create(recursive: true);
final cFileStream = cFile.openWrite();
if (preamble != null) {
cFileStream.writeln(preamble);
}
cFileStream.write(CPreludes.prelude);
ApiPreprocessor.preprocessAll(classesByName, config);
for (var packageName in packages.keys) {
final relativeFileName = '${packageName.replaceAll('.', '/')}.dart';
final dartFileUri = dartRoot.resolve(relativeFileName);
log.fine('Writing bindings for $packageName...');
final dartFile = await File.fromUri(dartFileUri).create(recursive: true);
final resolver = PackagePathResolver(
config.importMap ?? const {}, packageName, classNames,
predefined: {'java.lang.String': 'jni.JniString'});
final cgen = CBindingGenerator(config);
final dgen = DartBindingsGenerator(config, resolver);
final package = packages[packageName]!;
final cBindings = package.map(cgen.generateBinding).toList();
final dartBindings = package.map(dgen.generateBinding).toList();
// write imports from bindings
final dartFileStream = dartFile.openWrite();
final initImportPath = ('../' *
relativeFileName.codeUnits
.where((cu) => '/'.codeUnitAt(0) == cu)
.length) +
_initFileName;
if (preamble != null) {
dartFileStream.writeln(preamble);
}
dartFileStream
..write(DartBindingsGenerator.bindingFileHeaders)
..write(resolver.getImportStrings().join('\n'))
..write('import "$initImportPath" show jniLookup;\n\n');
// write dart bindings only after all imports are figured out
dartBindings.forEach(dartFileStream.write);
cBindings.forEach(cFileStream.write);
await dartFileStream.close();
}
await cFileStream.close();
await BindingsWriter.runDartFormat(dartRoot.toFilePath());
log.info('Copying auxiliary files...');
await _copyFileFromPackage(
'jni', 'src/dartjni.h', cRoot.resolve('$subdir/dartjni.h'));
await _copyFileFromPackage(
'jni', 'src/.clang-format', cRoot.resolve('$subdir/.clang-format'));
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 $exitCode');
}
} on ProcessException catch (e) {
log.warning('cannot run clang-format: $e');
}
log.info('Completed.');
}
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()}');
}
}
}