| // Copyright (c) 2020, 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 'package:path/path.dart' as p; | |
| import '../code_generator.dart'; | |
| import '../context.dart'; | |
| import '../strings.dart' as strings; | |
| import '../visitor/visitor.dart'; | |
| import 'utils.dart'; | |
| /// To store generated String bindings. | |
| class Writer { | |
| final Context context; | |
| final String header; | |
| /// Holds bindings, which lookup symbols. | |
| final List<Binding> lookUpBindings; | |
| /// Holds bindings, which lookup symbols through `FfiNative`. | |
| final List<Binding> ffiNativeBindings; | |
| /// Holds bindings which don't lookup symbols. | |
| final List<Binding> noLookUpBindings; | |
| /// The default asset id to use for [ffiNativeBindings]. | |
| final String? nativeAssetId; | |
| /// Manages the `_SymbolAddress` class. | |
| final SymbolAddressWriter symbolAddressWriter; | |
| final String? classDocComment; | |
| final bool generateForPackageObjectiveC; | |
| final List<String> nativeEntryPoints; | |
| /// Set true after calling [generate]. Indicates if | |
| /// [generateSymbolOutputYamlMap] can be called. | |
| bool get canGenerateSymbolOutput => _canGenerateSymbolOutput; | |
| bool _canGenerateSymbolOutput = false; | |
| Writer({ | |
| required this.lookUpBindings, | |
| required this.ffiNativeBindings, | |
| required this.noLookUpBindings, | |
| required this.nativeAssetId, | |
| this.classDocComment, | |
| this.header = '', | |
| required this.generateForPackageObjectiveC, | |
| required this.nativeEntryPoints, | |
| required this.context, | |
| }) : symbolAddressWriter = SymbolAddressWriter(context); | |
| /// Writes all bindings to a String. | |
| String generate() { | |
| final s = StringBuffer(); | |
| // We write the source first to determine which imports are actually | |
| // referenced. Headers and [s] are then combined into the final result. | |
| final result = StringBuffer(); | |
| // Write file header (if any). | |
| if (header.isNotEmpty) { | |
| result.writeln(header); | |
| } | |
| // Write auto generated declaration. | |
| result.write( | |
| makeDoc( | |
| 'AUTO GENERATED FILE, DO NOT EDIT.\n\nGenerated by `package:ffigen`.', | |
| ), | |
| ); | |
| // Write lint ignore if not specified by user already. | |
| final ignores = <String>[ | |
| if (!_hasLintIgnore(r'type\s*=\s*lint')) 'type=lint', | |
| // TODO(https://github.com/dart-lang/native/issues/2748): Remove the | |
| // unused_import ignore once we can guarantee that we don't generate | |
| // unused imports. | |
| if (!_hasLintIgnore('unused_import')) 'unused_import', | |
| if (!_hasLintIgnore('unused_element')) 'unused_element', | |
| if (!_hasLintIgnore('deprecated_member_use_from_same_package')) | |
| 'deprecated_member_use_from_same_package', | |
| ]; | |
| if (ignores.isNotEmpty) { | |
| result.write(makeDoc('ignore_for_file: ${ignores.join(', ')}')); | |
| } | |
| // If there are any @Native bindings, the file needs to have an | |
| // `@DefaultAsset` annotation for the symbols to resolve properly. This | |
| // avoids duplicating the asset on every element. | |
| // Since the annotation goes on a `library;` directive, it needs to appear | |
| // before other definitions in the file. | |
| final hasNativeBindings = | |
| ffiNativeBindings.isNotEmpty || | |
| noLookUpBindings.any((b) => b.hasNativeHelperFunctions); | |
| if (hasNativeBindings && nativeAssetId != null) { | |
| final ffiPrefix = context.libs.prefix(ffiImport); | |
| result | |
| ..writeln("@$ffiPrefix.DefaultAsset('$nativeAssetId')") | |
| ..writeln('library;\n'); | |
| } | |
| /// Write [lookUpBindings]. | |
| if (lookUpBindings.isNotEmpty) { | |
| final ffiPrefix = context.libs.prefix(ffiImport); | |
| final className = context.extraSymbols.wrapperClassName!.name; | |
| final lookupFn = context.extraSymbols.lookupFuncName!.name; | |
| // Write doc comment for wrapper class. | |
| s.write(makeDartDoc(classDocComment)); | |
| // Write wrapper class. | |
| s.write('class $className{\n'); | |
| // Write dylib. | |
| s.write('/// Holds the symbol lookup function.\n'); | |
| s.write( | |
| 'final $ffiPrefix.Pointer<T> Function<T extends ' | |
| '$ffiPrefix.NativeType>(String symbolName) $lookupFn;\n', | |
| ); | |
| s.write('\n'); | |
| //Write doc comment for wrapper class constructor. | |
| s.write(makeDartDoc('The symbols are looked up in [dynamicLibrary].')); | |
| // Write wrapper class constructor. | |
| s.write( | |
| '$className($ffiPrefix.DynamicLibrary dynamicLibrary): ' | |
| '$lookupFn = dynamicLibrary.lookup;\n\n', | |
| ); | |
| //Write doc comment for wrapper class named constructor. | |
| s.write(makeDartDoc('The symbols are looked up with [lookup].')); | |
| // Write wrapper class named constructor. | |
| s.write( | |
| '$className.fromLookup($ffiPrefix.Pointer<T> ' | |
| 'Function<T extends $ffiPrefix.NativeType>(' | |
| 'String symbolName) lookup): $lookupFn = lookup;\n\n', | |
| ); | |
| for (final b in lookUpBindings) { | |
| s.write(b.toBindingString(this).string); | |
| } | |
| if (symbolAddressWriter.shouldGenerate && | |
| symbolAddressWriter.hasNonNativeAddress) { | |
| s.write(symbolAddressWriter.writeObject(this)); | |
| } | |
| s.write('}\n\n'); | |
| } | |
| if (ffiNativeBindings.isNotEmpty) { | |
| for (final b in ffiNativeBindings) { | |
| s.write(b.toBindingString(this).string); | |
| } | |
| } | |
| if (symbolAddressWriter.shouldGenerate) { | |
| if (!symbolAddressWriter.hasNonNativeAddress) { | |
| s.write(symbolAddressWriter.writeObject(this)); | |
| } | |
| s.write(symbolAddressWriter.writeClass(this)); | |
| } | |
| /// Write [noLookUpBindings]. | |
| for (final b in noLookUpBindings) { | |
| s.write(b.toBindingString(this).string); | |
| } | |
| // Write neccesary imports. | |
| final libs = context.libs.used.toList() | |
| ..sort((l1, l2) => l1.name.compareTo(l2.name)); | |
| for (final lib in libs) { | |
| final path = lib.importPath(generateForPackageObjectiveC); | |
| result.write("import '$path' as ${context.libs.prefix(lib)};\n"); | |
| } | |
| if (libs.contains(objcPkgImport)) { | |
| final objcPrefix = context.libs.prefix(objcPkgImport); | |
| result.write(''' | |
| const _\$objcVersionCheck = $objcPrefix.ObjCVersionCheck( | |
| $objcMajorVersion, $objcMinorVersion); | |
| '''); | |
| } | |
| result.write(s); | |
| // Warn about Enum usage in API surface. | |
| final notEnums = _allBindings.where( | |
| (b) => b is! Type || (b as Type).typealiasType is! EnumClass, | |
| ); | |
| final usedEnums = visit( | |
| context, | |
| _FindEnumsVisitation(), | |
| notEnums, | |
| ).enums.where((e) => !e.silenceWarning); | |
| if (usedEnums.isNotEmpty) { | |
| final names = usedEnums.map((e) => e.originalName).toList()..sort(); | |
| context.logger.severe( | |
| 'The integer type used for enums is ' | |
| 'implementation-defined. FFIgen tries to mimic the integer sizes ' | |
| 'chosen by the most common compilers for the various OS and ' | |
| 'architecture combinations. To prevent any crashes, remove the ' | |
| 'enums from your API surface. To rely on the (unsafe!) mimicking, ' | |
| 'you can silence this warning by setting node.silenceWarning = true ' | |
| 'in a Visitor(enumClass: ...). Affected enums:' | |
| '\n\t${names.join('\n\t')}', | |
| ); | |
| } | |
| _canGenerateSymbolOutput = true; | |
| return result.toString(); | |
| } | |
| List<Binding> get _allBindings => <Binding>[ | |
| ...noLookUpBindings, | |
| ...ffiNativeBindings, | |
| ...lookUpBindings, | |
| ]; | |
| Map<String, dynamic> generateSymbolOutputYamlMap(String importFilePath) { | |
| final bindings = _allBindings; | |
| if (!canGenerateSymbolOutput) { | |
| throw Exception( | |
| 'Invalid state: generateSymbolOutputYamlMap() ' | |
| 'called before generate()', | |
| ); | |
| } | |
| // Warn for macros. | |
| final hasMacroBindings = bindings.any( | |
| (element) => element is MacroConstant, | |
| ); | |
| if (hasMacroBindings) { | |
| context.logger.info( | |
| 'Removing all Macros from symbol file since they cannot ' | |
| 'be cross referenced reliably.', | |
| ); | |
| } | |
| // Remove internal bindings and macros. | |
| bindings.removeWhere((element) { | |
| return element.isInternal || (element is MacroConstant); | |
| }); | |
| // Sort bindings alphabetically by USR. | |
| bindings.sort((a, b) => a.usr.compareTo(b.usr)); | |
| final usesFfiNative = bindings.whereType<Func>().any( | |
| (element) => element.loadFromNativeAsset, | |
| ); | |
| return { | |
| strings.formatVersion: strings.symbolFileFormatVersion, | |
| strings.files: { | |
| importFilePath: { | |
| strings.usedConfig: {strings.ffiNative: usesFfiNative}, | |
| strings.symbols: { | |
| for (final b in bindings) b.usr: _makeSymbolMapValue(b), | |
| }, | |
| }, | |
| }, | |
| }; | |
| } | |
| bool _hasLintIgnore(String ignore) => | |
| RegExp('ignore_for_file:\\s*$ignore').hasMatch(header); | |
| Map<String, String> _makeSymbolMapValue(Binding b) { | |
| final dartName = b is Typealias ? getTypedefDartAliasName(b) : null; | |
| return {strings.name: b.name, strings.dartName: ?dartName}; | |
| } | |
| String? getTypedefDartAliasName(Type b) { | |
| if (b is! Typealias) return null; | |
| return b.dartAliasName?.name ?? getTypedefDartAliasName(b.type); | |
| } | |
| static String _objcImport(String entryPoint, String outDir) { | |
| final frameworkHeader = parseObjCFrameworkHeader(entryPoint); | |
| if (frameworkHeader == null) { | |
| // If it's not a framework header, use a relative import. | |
| return '#import "${p.relative(entryPoint, from: outDir)}"\n'; | |
| } | |
| // If it's a framework header, use a <> style import. | |
| return '#import <$frameworkHeader>\n'; | |
| } | |
| String? generateRecordUseMapping() { | |
| final recordUseMappings = { | |
| ...lookUpBindings.whereType<Func>(), | |
| ...ffiNativeBindings.whereType<Func>(), | |
| }.toRecordUseMap(); | |
| if (recordUseMappings.isEmpty) { | |
| return null; | |
| } | |
| final s = StringBuffer(); | |
| // Write file header (if any). | |
| if (header.isNotEmpty) { | |
| s.writeln(header); | |
| } | |
| // Write auto generated declaration. | |
| s.write( | |
| makeDoc( | |
| 'AUTO GENERATED FILE, DO NOT EDIT.\n\nGenerated by `package:ffigen`.', | |
| ), | |
| ); | |
| s.writeln('// ignore_for_file: type=lint'); | |
| s.writeln(); | |
| s.writeln('/// Mapping from Dart function name to native symbol name.'); | |
| s.writeln('const recordUseMapping = {'); | |
| for (final entry in recordUseMappings.entries) { | |
| s.writeln(" '${entry.key}': '${entry.value}',"); | |
| } | |
| s.writeln('};'); | |
| return s.toString(); | |
| } | |
| /// Writes the Objective C code needed for the bindings, if any. Returns null | |
| /// if there are no bindings that need generated ObjC code. This function does | |
| /// not generate the output file, but the [outFilename] does affect the | |
| /// generated code. | |
| String? generateObjC(String outFilename) { | |
| final outDir = p.dirname(outFilename); | |
| final s = StringBuffer(); | |
| s.write(''' | |
| #include <stdint.h> | |
| #import <Foundation/Foundation.h> | |
| #import <objc/message.h> | |
| '''); | |
| for (final entryPoint in nativeEntryPoints) { | |
| s.write(_objcImport(entryPoint, outDir)); | |
| } | |
| s.write(r''' | |
| #if !__has_feature(objc_arc) | |
| #error "This file must be compiled with ARC enabled" | |
| #endif | |
| #pragma clang diagnostic push | |
| #pragma clang diagnostic ignored "-Wundeclared-selector" | |
| typedef struct { | |
| int64_t version; | |
| void* (*newWaiter)(void); | |
| void (*awaitWaiter)(void*); | |
| void* (*currentIsolate)(void); | |
| void (*enterIsolate)(void*); | |
| void (*exitIsolate)(void); | |
| int64_t (*getMainPortId)(void); | |
| bool (*getCurrentThreadOwnsIsolate)(int64_t); | |
| void (*invokeListenerPortBlock)(int64_t port, void*); | |
| void (*invokeBlockingPortBlock)(int64_t port, void*, void*); | |
| } DOBJC_Context; | |
| id objc_retainBlock(id); | |
| #define BLOCKING_BLOCK_IMPL(ctx, TYPE, SIG, INVOKE_DIRECT, INVOKE_LISTENER) \ | |
| assert(ctx->version >= 1); \ | |
| void* targetIsolate = ctx->currentIsolate(); \ | |
| int64_t targetPort = ctx->getMainPortId == NULL ? 0 : ctx->getMainPortId(); \ | |
| __block __weak TYPE weakSelfBlock = nil; \ | |
| TYPE strongSelfBlock = [SIG { \ | |
| void* currentIsolate = ctx->currentIsolate(); \ | |
| bool mayEnterIsolate = \ | |
| currentIsolate == NULL && \ | |
| ctx->getCurrentThreadOwnsIsolate != NULL && \ | |
| ctx->getCurrentThreadOwnsIsolate(targetPort); \ | |
| if (currentIsolate == targetIsolate || mayEnterIsolate) { \ | |
| if (mayEnterIsolate) { \ | |
| ctx->enterIsolate(targetIsolate); \ | |
| } \ | |
| INVOKE_DIRECT; \ | |
| if (mayEnterIsolate) { \ | |
| ctx->exitIsolate(); \ | |
| } \ | |
| } else { \ | |
| void* waiter = ctx->newWaiter(); \ | |
| TYPE selfRetain = [weakSelfBlock copy]; \ | |
| INVOKE_LISTENER; \ | |
| ctx->awaitWaiter(waiter); \ | |
| (void)selfRetain; \ | |
| } \ | |
| } copy]; \ | |
| weakSelfBlock = strongSelfBlock; \ | |
| return strongSelfBlock; | |
| '''); | |
| var empty = true; | |
| for (final binding in _allBindings) { | |
| final bindingString = binding.toObjCBindingString(this); | |
| if (bindingString != null) { | |
| empty = false; | |
| s.write(bindingString.string); | |
| } | |
| } | |
| s.write(''' | |
| #undef BLOCKING_BLOCK_IMPL | |
| #pragma clang diagnostic pop | |
| '''); | |
| return empty ? null : s.toString(); | |
| } | |
| /// Writes the Cpp glue code needed for the bindings, if any. Returns null | |
| /// if there are no CppClass bindings. | |
| String? generateCpp(String outFilename) { | |
| final s = StringBuffer(); | |
| final outDir = p.dirname(outFilename); | |
| s.write('#include <memory>\n'); | |
| // Emit each entry-point header exactly once. | |
| for (final header in context.config.input.entryPoints) { | |
| s.write('#include "${p.relative(header.toFilePath(), from: outDir)}"\n'); | |
| } | |
| s.write(r''' | |
| #if defined(_WIN32) | |
| #define FFIGEN_EXPORT __declspec(dllexport) | |
| #else | |
| #define FFIGEN_EXPORT | |
| #endif | |
| extern "C" { | |
| '''); | |
| var empty = true; | |
| for (final binding in _allBindings) { | |
| final cppBindingString = binding.toCppBindingString(this); | |
| if (cppBindingString != null) { | |
| empty = false; | |
| s.write(cppBindingString); | |
| } | |
| } | |
| s.write('}\n'); | |
| return empty ? null : s.toString(); | |
| } | |
| } | |
| /// Manages the generated `_SymbolAddress` class. | |
| class SymbolAddressWriter { | |
| final Context context; | |
| final List<_SymbolAddressUnit> _addresses = []; | |
| /// Used to check if we need to generate `_SymbolAddress` class. | |
| bool get shouldGenerate => _addresses.isNotEmpty; | |
| bool hasNonNativeAddress = false; | |
| SymbolAddressWriter(this.context); | |
| late final _symbolAddressClassName = context.rootScope.addPrivate( | |
| '_SymbolAddresses', | |
| ); | |
| void addSymbol({ | |
| required String type, | |
| required String name, | |
| required String ptrName, | |
| }) { | |
| hasNonNativeAddress = true; | |
| _addresses.add(_SymbolAddressUnit(type, name, ptrName, false)); | |
| } | |
| void addNativeSymbol({required String type, required String name}) { | |
| _addresses.add(_SymbolAddressUnit(type, name, '', true)); | |
| } | |
| String writeObject(Writer w) { | |
| final fieldName = context.extraSymbols.symbolAddressVariableName.name; | |
| if (hasNonNativeAddress) { | |
| return 'late final $fieldName = $_symbolAddressClassName(this);'; | |
| } else { | |
| return 'const $fieldName = $_symbolAddressClassName();'; | |
| } | |
| } | |
| String writeClass(Writer w) { | |
| final sb = StringBuffer(); | |
| sb.write('class $_symbolAddressClassName {\n'); | |
| late final libraryVarName = context.rootScope.addPrivate('_library'); | |
| if (hasNonNativeAddress) { | |
| // Write Library object. | |
| final wrapperClassName = context.extraSymbols.wrapperClassName!.name; | |
| sb.write(' final $wrapperClassName $libraryVarName;\n'); | |
| // Write Constructor. | |
| sb.write(' $_symbolAddressClassName(this.$libraryVarName);\n'); | |
| } else { | |
| // Native bindings are top-level, so we don't need a field here. | |
| sb.write(' const $_symbolAddressClassName();'); | |
| } | |
| for (final address in _addresses) { | |
| sb.write(' ${address.type} get ${address.name} => '); | |
| if (address.native) { | |
| // For native fields and functions, we can use Native.addressOf to look | |
| // up their address. | |
| // The name of address getter shadows the actual element in the library, | |
| // so we need to use a self-import. | |
| final arg = '${context.libs.prefix(selfImport)}.${address.name}'; | |
| sb.writeln('${context.libs.prefix(ffiImport)}.Native.addressOf($arg);'); | |
| } else { | |
| // For other elements, the generator will write a private field of type | |
| // Pointer which we can reference here. | |
| sb.writeln('$libraryVarName.${address.ptrName};'); | |
| } | |
| } | |
| sb.write('}\n'); | |
| return sb.toString(); | |
| } | |
| } | |
| /// Holds the data for a single symbol address. | |
| class _SymbolAddressUnit { | |
| final String type, name, ptrName; | |
| /// Whether the symbol we're looking up has been declared with `@Native`. | |
| final bool native; | |
| _SymbolAddressUnit(this.type, this.name, this.ptrName, this.native); | |
| } | |
| class _FindEnumsVisitation extends Visitation { | |
| final enums = <EnumClass>{}; | |
| @override | |
| void visitEnumClass(EnumClass node) { | |
| node.visitChildren(visitor); | |
| enums.add(node); | |
| } | |
| } |