| // 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; |
| |
| final bool silenceEnumWarning; |
| |
| Writer({ |
| required this.lookUpBindings, |
| required this.ffiNativeBindings, |
| required this.noLookUpBindings, |
| required this.nativeAssetId, |
| List<LibraryImport> additionalImports = const <LibraryImport>[], |
| this.classDocComment, |
| this.header, |
| required this.generateForPackageObjectiveC, |
| required this.silenceEnumWarning, |
| 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 != null) { |
| 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. |
| if (!RegExp(r'ignore_for_file:\s*type\s*=\s*lint').hasMatch(header ?? '')) { |
| result.write(makeDoc('ignore_for_file: type=lint')); |
| } |
| |
| // 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. |
| if (ffiNativeBindings.isNotEmpty && 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"); |
| } |
| result.write(s); |
| |
| // Warn about Enum usage in API surface. |
| if (!silenceEnumWarning) { |
| final notEnums = _allBindings.where( |
| (b) => b is! Type || (b as Type).typealiasType is! EnumClass, |
| ); |
| final usedEnums = visit(context, _FindEnumsVisitation(), notEnums).enums; |
| 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 adding silence-enum-warning: true ' |
| 'to the FFIgen config. 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 Constant && element.usr.contains('@macro@'), |
| ); |
| 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 Constant && element.usr.contains('@macro@')); |
| }); |
| |
| // 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), |
| }, |
| }, |
| }, |
| }; |
| } |
| |
| Map<String, String> _makeSymbolMapValue(Binding b) { |
| final dartName = b is Typealias ? getTypedefDartAliasName(b) : null; |
| return { |
| strings.name: b.name, |
| if (dartName != null) 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'; |
| } |
| |
| /// 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); |
| } DOBJC_Context; |
| |
| id objc_retainBlock(id); |
| |
| #define BLOCKING_BLOCK_IMPL(ctx, BLOCK_SIG, INVOKE_DIRECT, INVOKE_LISTENER) \ |
| assert(ctx->version >= 1); \ |
| void* targetIsolate = ctx->currentIsolate(); \ |
| int64_t targetPort = ctx->getMainPortId == NULL ? 0 : ctx->getMainPortId(); \ |
| return BLOCK_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(); \ |
| INVOKE_LISTENER; \ |
| ctx->awaitWaiter(waiter); \ |
| } \ |
| }; |
| |
| '''); |
| |
| 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(); |
| } |
| } |
| |
| /// 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); |
| } |
| } |