| // Copyright (c) 2026, 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. |
| |
| // Runs the FFIgen configs, then merges tool/data/extra_methods.dart.in into the |
| // Objective C bindings. |
| |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:ffigen/ffigen.dart'; |
| import 'package:logging/logging.dart'; |
| |
| const assetId = 'package:objective_c/objective_c.dylib'; |
| const runtimeBindings = 'lib/src/runtime_bindings_generated.dart'; |
| const cBindings = 'lib/src/c_bindings_generated.dart'; |
| const objcBindings = 'lib/src/objective_c_bindings_generated.dart'; |
| |
| const objcInterfaces = { |
| 'DOBJCDartInputStreamAdapter': 'DartInputStreamAdapter', |
| 'DOBJCDartInputStreamAdapterWeakHolder': 'DartInputStreamAdapterWeakHolder', |
| 'DOBJCObservation': 'DOBJCObservation', |
| 'DOBJCDartProtocolBuilder': 'DartProtocolBuilder', |
| 'DOBJCDartProtocol': 'DartProtocol', |
| 'NSArray': 'NSArray', |
| 'NSAttributedString': 'NSAttributedString', |
| 'NSAttributedStringMarkdownParsingOptions': |
| 'NSAttributedStringMarkdownParsingOptions', |
| 'NSBundle': 'NSBundle', |
| 'NSCharacterSet': 'NSCharacterSet', |
| 'NSCoder': 'NSCoder', |
| 'NSData': 'NSData', |
| 'NSDate': 'NSDate', |
| 'NSDictionary': 'NSDictionary', |
| 'NSEnumerator': 'NSEnumerator', |
| 'NSError': 'NSError', |
| 'NSIndexSet': 'NSIndexSet', |
| 'NSInputStream': 'NSInputStream', |
| 'NSInvocation': 'NSInvocation', |
| 'NSItemProvider': 'NSItemProvider', |
| 'NSLocale': 'NSLocale', |
| 'NSMethodSignature': 'NSMethodSignature', |
| 'NSMutableArray': 'NSMutableArray', |
| 'NSMutableData': 'NSMutableData', |
| 'NSMutableDictionary': 'NSMutableDictionary', |
| 'NSMutableIndexSet': 'NSMutableIndexSet', |
| 'NSMutableOrderedSet': 'NSMutableOrderedSet', |
| 'NSMutableSet': 'NSMutableSet', |
| 'NSMutableString': 'NSMutableString', |
| 'NSNotification': 'NSNotification', |
| 'NSNull': 'NSNull', |
| 'NSNumber': 'NSNumber', |
| 'NSObject': 'NSObject', |
| 'NSOutputStream': 'NSOutputStream', |
| 'NSOrderedCollectionChange': 'NSOrderedCollectionChange', |
| 'NSOrderedCollectionDifference': 'NSOrderedCollectionDifference', |
| 'NSOrderedSet': 'NSOrderedSet', |
| 'NSPort': 'NSPort', |
| 'NSPortMessage': 'NSPortMessage', |
| 'NSProgress': 'NSProgress', |
| 'NSRunLoop': 'NSRunLoop', |
| 'NSSet': 'NSSet', |
| 'NSStream': 'NSStream', |
| 'NSString': 'NSString', |
| 'NSTimer': 'NSTimer', |
| 'NSURL': 'NSURL', |
| 'NSURLHandle': 'NSURLHandle', |
| 'NSValue': 'NSValue', |
| 'Protocol': 'Protocol', |
| }; |
| |
| const objcProtocols = { |
| 'NSCoding': 'NSCoding', |
| 'NSCopying': 'NSCopying', |
| 'NSFastEnumeration': 'NSFastEnumeration', |
| 'NSItemProviderReading': 'NSItemProviderReading', |
| 'NSItemProviderWriting': 'NSItemProviderWriting', |
| 'NSMutableCopying': 'NSMutableCopying', |
| 'NSObject': 'NSObjectProtocol', |
| 'NSPortDelegate': 'NSPortDelegate', |
| 'NSSecureCoding': 'NSSecureCoding', |
| 'NSStreamDelegate': 'NSStreamDelegate', |
| 'Observer': 'Observer', |
| }; |
| |
| const objcCategories = { |
| 'NSArrayCreation', |
| 'NSAttributedStringCreateFromMarkdown', |
| 'NSAttributedStringFormatting', |
| 'NSDataBase64Encoding', |
| 'NSDataCompression', |
| 'NSDataCreation', |
| 'NSDateCreation', |
| 'NSDictionaryCreation', |
| 'NSExtendedArray', |
| 'NSExtendedAttributedString', |
| 'NSExtendedData', |
| 'NSExtendedDate', |
| 'NSExtendedDictionary', |
| 'NSExtendedEnumerator', |
| 'NSExtendedMutableArray', |
| 'NSExtendedMutableData', |
| 'NSExtendedMutableDictionary', |
| 'NSExtendedMutableOrderedSet', |
| 'NSExtendedMutableSet', |
| 'NSExtendedOrderedSet', |
| 'NSExtendedSet', |
| 'NSInputStreamExtensions', |
| 'NSLocaleCreation', |
| 'NSMutableArrayCreation', |
| 'NSMutableDataCreation', |
| 'NSMutableDictionaryCreation', |
| 'NSMutableOrderedSetCreation', |
| 'NSMutableSetCreation', |
| 'NSNotificationCreation', |
| 'NSNumberCreation', |
| 'NSNumberIsBool', |
| 'NSNumberIsFloat', |
| 'NSOrderedSetCreation', |
| 'NSOutputStreamExtensions', |
| 'NSSetCreation', |
| 'NSStringExtensionMethods', |
| }; |
| |
| const objcStructs = { |
| 'AEDesc': 'AEDesc', |
| '__CFRunLoop': 'CFRunLoop', |
| '__CFString': 'CFString', |
| 'CGPoint': 'CGPoint', |
| 'CGRect': 'CGRect', |
| 'CGSize': 'CGSize', |
| 'NSEdgeInsets': 'NSEdgeInsets', |
| 'NSFastEnumerationState': 'NSFastEnumerationState', |
| '_NSRange': 'NSRange', |
| '_NSZone': 'NSZone', |
| 'OpaqueAEDataStorageType': 'OpaqueAEDataStorageType', |
| }; |
| |
| const objcEnums = { |
| 'NSAppleEventSendOptions', |
| 'NSAttributedStringEnumerationOptions', |
| 'NSAttributedStringFormattingOptions', |
| 'NSAttributedStringMarkdownInterpretedSyntax', |
| 'NSAttributedStringMarkdownParsingFailurePolicy', |
| 'NSBinarySearchingOptions', |
| 'NSCollectionChangeType', |
| 'NSComparisonResult', |
| 'NSDataBase64DecodingOptions', |
| 'NSDataBase64EncodingOptions', |
| 'NSDataCompressionAlgorithm', |
| 'NSDataReadingOptions', |
| 'NSDataSearchOptions', |
| 'NSDataWritingOptions', |
| 'NSDecodingFailurePolicy', |
| 'NSEnumerationOptions', |
| 'NSItemProviderFileOptions', |
| 'NSItemProviderRepresentationVisibility', |
| 'NSKeyValueChange', |
| 'NSKeyValueObservingOptions', |
| 'NSKeyValueSetMutationKind', |
| 'NSLinguisticTaggerOptions', |
| 'NSLocaleLanguageDirection', |
| 'NSOrderedCollectionDifferenceCalculationOptions', |
| 'NSPropertyListFormat', |
| 'NSQualityOfService', |
| 'NSSortOptions', |
| 'NSStreamEvent', |
| 'NSStreamStatus', |
| 'NSStringCompareOptions', |
| 'NSStringEncodingConversionOptions', |
| 'NSStringEnumerationOptions', |
| 'NSURLBookmarkCreationOptions', |
| 'NSURLBookmarkResolutionOptions', |
| 'NSURLHandleStatus', |
| }; |
| |
| Future<void> main(List<String> args) async { |
| final argResults = |
| (ArgParser()..addFlag( |
| 'format', |
| help: 'Format the generated code.', |
| defaultsTo: true, |
| negatable: true, |
| )) |
| .parse(args); |
| await run(format: argResults.flag('format')); |
| } |
| |
| Future<void> run({required bool format, Uri? packageRoot}) async { |
| packageRoot ??= _defaultPackageRoot(); |
| final logger = Logger.root; |
| logger.level = Level.SEVERE; |
| |
| final runtimeBindingsPath = packageRoot.resolve(runtimeBindings).toFilePath(); |
| final cBindingsPath = packageRoot.resolve(cBindings).toFilePath(); |
| final objcBindingsPath = packageRoot.resolve(objcBindings).toFilePath(); |
| final objcExportsPath = packageRoot |
| .resolve('lib/src/objective_c_bindings_exported.dart') |
| .toFilePath(); |
| final extraMethodsFilePath = packageRoot |
| .resolve('tool/data/extra_methods.dart.in') |
| .toFilePath(); |
| final builtInTypesPath = packageRoot |
| .resolve('../ffigen/lib/src/code_generator/objc_built_in_types.dart') |
| .toFilePath(); |
| final interfaceListTestPath = packageRoot |
| .resolve('test/interface_lists_test.dart') |
| .toFilePath(); |
| |
| print('Generating runtime bindings...'); |
| await getRuntimeConfig(packageRoot).generate(logger: logger); |
| |
| print('Generating C bindings...'); |
| await getCConfig(packageRoot).generate(logger: logger); |
| |
| print('Generating ObjC bindings...'); |
| await getObjCConfig(packageRoot).generate(logger: logger); |
| mergeExtraMethods(objcBindingsPath, parseExtraMethods(extraMethodsFilePath)); |
| |
| print('Generating objc_built_in_types.dart...'); |
| final exports = writeBuiltInTypes(builtInTypesPath); |
| |
| print('Generating objc_bindings_exported.dart...'); |
| writeExports(exports, objcExportsPath); |
| |
| if (format) { |
| print('Formatting bindings...'); |
| dartCmd([ |
| 'format', |
| runtimeBindingsPath, |
| cBindingsPath, |
| objcBindingsPath, |
| builtInTypesPath, |
| objcExportsPath, |
| ], workingDir: packageRoot.toFilePath()); |
| } |
| |
| print('Running tests...'); |
| dartCmd([ |
| 'test', |
| interfaceListTestPath, |
| ], workingDir: packageRoot.toFilePath()); |
| } |
| |
| List<String> writeBuiltInTypes(String out) { |
| final s = StringBuffer(); |
| final exports = <String>{}; |
| |
| s.write(''' |
| // Copyright (c) 2025, 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. |
| |
| // Generated by package:objective_c's tool/ffigen.dart. |
| '''); |
| |
| void writeMap(String name, Map<String, String> map) { |
| exports.addAll(map.values); |
| s.write(''' |
| |
| const $name = { |
| ${map.entries.map((e) => " '${e.key}': '${e.value}',").join('\n')} |
| }; |
| '''); |
| } |
| |
| void writeSet(String name, Set<String> set) { |
| exports.addAll(set); |
| s.write(''' |
| |
| const $name = { |
| ${set.map((e) => " '$e',").join('\n')} |
| }; |
| '''); |
| } |
| |
| writeMap('objCBuiltInInterfaces', objcInterfaces); |
| exports.addAll([for (final name in objcInterfaces.values) '$name\$Methods']); |
| writeMap('objCBuiltInCompounds', objcStructs); |
| writeSet('objCBuiltInEnums', objcEnums); |
| writeMap('objCBuiltInProtocols', objcProtocols); |
| exports.addAll([for (final name in objcProtocols.values) '$name\$Methods']); |
| exports.addAll([for (final name in objcProtocols.values) '$name\$Builder']); |
| writeSet('objCBuiltInCategories', objcCategories); |
| |
| File(out).writeAsStringSync(s.toString()); |
| |
| return exports.toList()..sort(); |
| } |
| |
| void writeExports(List<String> exports, String out) { |
| File(out).writeAsStringSync(''' |
| // Copyright (c) 2025, 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. |
| |
| // Generated by package:objective_c's tool/ffigen.dart. |
| |
| export 'objective_c_bindings_generated.dart' |
| show |
| ${exports.join(',\n ')}; |
| '''); |
| } |
| |
| FfiGenerator getRuntimeConfig([Uri? packageRoot]) { |
| packageRoot ??= _defaultPackageRoot(); |
| |
| return FfiGenerator( |
| output: Output( |
| dart: DartOutput(path: packageRoot.resolve(runtimeBindings)), |
| style: const NativeExternalBindings(), |
| format: false, |
| preamble: ''' |
| // Copyright (c) 2024, 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. |
| |
| // Bindings for `src/objective_c_runtime.h`. |
| // Regenerate bindings with `dart run tool/ffigen.dart`. |
| |
| // ignore_for_file: always_specify_types |
| // ignore_for_file: camel_case_types |
| // ignore_for_file: non_constant_identifier_names |
| // ignore_for_file: unused_element |
| // coverage:ignore-file |
| ''', |
| ), |
| input: Input( |
| entryPoints: [packageRoot.resolve('src/objective_c_runtime.h')], |
| ), |
| objectiveC: const ObjectiveC( |
| // ignore: deprecated_member_use, deprecated_member_use_from_same_package |
| generateForPackageObjectiveC: true, |
| ), |
| visitors: [ |
| Visitor( |
| func: (node) { |
| const funcRenames = { |
| 'objc_autorelease': 'objectAutorelease', |
| 'objc_autoreleasePoolPop': 'autoreleasePoolPop', |
| 'objc_autoreleasePoolPush': 'autoreleasePoolPush', |
| 'objc_copyClassList': 'copyClassList', |
| 'objc_getClass': 'getClass', |
| 'objc_getProtocol': 'getProtocol', |
| 'objc_msgSend': 'msgSend', |
| 'objc_msgSend_fpret': 'msgSendFpret', |
| 'objc_msgSend_stret': 'msgSendStret', |
| 'objc_release': 'objectRelease', |
| 'objc_retain': 'objectRetain', |
| 'objc_retainBlock': 'blockRetain', |
| 'object_getClass': 'getObjectClass', |
| 'protocol_getMethodDescription': 'getMethodDescription', |
| 'protocol_getName': 'getProtocolName', |
| 'sel_getName': 'getName', |
| 'sel_registerName': 'registerName', |
| }; |
| if (funcRenames[node.name] case final renamed?) { |
| node.isIncluded = true; |
| node.name = renamed; |
| } |
| node.isLeaf = !node.originalName.startsWith('objc_msgSend'); |
| }, |
| global: (node) { |
| const included = { |
| 'NSKeyValueChangeIndexesKey', |
| 'NSKeyValueChangeKindKey', |
| 'NSKeyValueChangeNewKey', |
| 'NSKeyValueChangeNotificationIsPriorKey', |
| 'NSKeyValueChangeOldKey', |
| 'NSLocalizedDescriptionKey', |
| }; |
| node.isIncluded = |
| included.contains(node.name) || |
| (node.name.startsWith('_NSConcrete') && |
| node.name.endsWith('Block')); |
| if (node.name.startsWith('_')) { |
| node.name = node.name.substring(1); |
| } |
| }, |
| struct: (node) { |
| if (node.name.startsWith('_ObjC')) { |
| node.isIncluded = true; |
| node.name = node.name.substring(1); |
| } |
| }, |
| ), |
| ], |
| ); |
| } |
| |
| FfiGenerator getCConfig([Uri? packageRoot]) { |
| packageRoot ??= _defaultPackageRoot(); |
| |
| return FfiGenerator( |
| output: Output( |
| dart: DartOutput(path: packageRoot.resolve(cBindings)), |
| style: const NativeExternalBindings(assetId: assetId), |
| format: false, |
| preamble: ''' |
| // Copyright (c) 2024, 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. |
| |
| // Bindings for `src/objective_c.h` etc. |
| // Regenerate bindings with `dart run tool/ffigen.dart`. |
| |
| // coverage:ignore-file |
| ''', |
| ), |
| input: Input( |
| entryPoints: [ |
| packageRoot.resolve('src/include/dart_api_dl.h'), |
| packageRoot.resolve('src/objective_c.h'), |
| packageRoot.resolve('src/os_version.h'), |
| ], |
| ), |
| objectiveC: const ObjectiveC( |
| // ignore: deprecated_member_use, deprecated_member_use_from_same_package |
| generateForPackageObjectiveC: true, |
| ), |
| visitors: [ |
| Visitor( |
| func: (node) { |
| if (node.name.startsWith('DOBJC_')) { |
| node.isIncluded = true; |
| node.name = node.name.substring(6); |
| } else if (node.name == 'newFinalizableHandle') { |
| node.isIncluded = true; |
| } |
| const nonLeaf = { |
| 'DOBJC_deleteFinalizableHandle', |
| 'DOBJC_disposeObjCBlockWithClosure', |
| 'DOBJC_newFinalizableBool', |
| 'DOBJC_newFinalizableHandle', |
| 'DOBJC_awaitWaiter', |
| 'DOBJC_invokeListenerPortBlock', |
| 'DOBJC_invokeBlockingPortBlock', |
| }; |
| node.isLeaf = !nonLeaf.contains(node.originalName); |
| }, |
| typealias: (node) { |
| if (node.name == 'Dart_FinalizableHandle') { |
| node.isIncluded = .always; |
| } |
| }, |
| struct: (node) { |
| node.dependencies = CompoundDependencies.opaque; |
| const included = {'_ObjCBlockImpl', '_ObjCBlockDesc', '_Version'}; |
| node.isIncluded = included.contains(node.name); |
| if (node.name == '_Dart_FinalizableHandle') { |
| node.name = 'Dart_FinalizableHandle_'; |
| } else if (node.name.startsWith('_')) { |
| node.name = node.name.substring(1); |
| } |
| }, |
| macroConstant: (node) { |
| node.isIncluded = node.name == 'ILLEGAL_PORT'; |
| }, |
| ), |
| ], |
| ); |
| } |
| |
| FfiGenerator getObjCConfig([Uri? packageRoot]) { |
| packageRoot ??= _defaultPackageRoot(); |
| return FfiGenerator( |
| output: Output( |
| dart: DartOutput(path: packageRoot.resolve(objcBindings)), |
| objectiveCFile: packageRoot.resolve( |
| 'src/objective_c_bindings_generated.m', |
| ), |
| style: const NativeExternalBindings(assetId: assetId), |
| format: false, |
| preamble: ''' |
| // Copyright (c) 2024, 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. |
| |
| // Bindings for package:objective_c's ObjC code and the Foundation framework. |
| // Regenerate bindings with `dart run tool/ffigen.dart`. |
| |
| // coverage:ignore-file |
| ''', |
| ), |
| input: Input( |
| entryPoints: [ |
| packageRoot.resolve('src/foundation.h'), |
| packageRoot.resolve('src/input_stream_adapter.h'), |
| packageRoot.resolve('src/ns_number.h'), |
| packageRoot.resolve('src/observer.h'), |
| packageRoot.resolve('src/protocol.h'), |
| ], |
| ), |
| objectiveC: ObjectiveC( |
| // ignore: deprecated_member_use, deprecated_member_use_from_same_package |
| generateForPackageObjectiveC: true, |
| externalVersions: ExternalVersions( |
| ios: Versions(min: Version(12, 0, 0)), |
| macos: Versions(min: Version(10, 14, 0)), |
| ), |
| ), |
| visitors: [ |
| Visitor( |
| objCInterface: (node) { |
| node.includeCategories = false; |
| if (objcInterfaces[node.originalName] case final renamed?) { |
| node.isIncluded = true; |
| node.name = renamed; |
| } |
| }, |
| objCProtocol: (node) { |
| if (objcProtocols[node.originalName] case final renamed?) { |
| node.isIncluded = true; |
| node.name = renamed; |
| } |
| }, |
| objCCategory: (node) { |
| if (objcCategories.contains(node.originalName)) { |
| node.isIncluded = true; |
| } |
| }, |
| struct: (node) { |
| if (objcStructs[node.originalName] case final renamed?) { |
| node.isIncluded = true; |
| node.name = renamed; |
| } |
| }, |
| enumClass: (node) { |
| if (objcEnums.contains(node.originalName)) { |
| node.isIncluded = true; |
| } |
| }, |
| typealias: (node) { |
| const included = {'CFStringRef'}; |
| node.isIncluded = included.contains(node.originalName) |
| ? TypealiasInclude.always |
| : TypealiasInclude.never; |
| }, |
| objCMethod: (node) { |
| if (node.parent.originalName == 'NSBundle' && |
| node.originalName == |
| 'localizedStringForKey:value:table:localizations:') { |
| node.isIncluded = false; |
| } |
| }, |
| ), |
| ], |
| ); |
| } |
| |
| Uri _defaultPackageRoot() { |
| if (Platform.script.isScheme('file')) { |
| final scriptFile = File.fromUri(Platform.script); |
| var dir = scriptFile.parent; |
| while (dir.path != dir.parent.path) { |
| final pubspec = File('${dir.path}/pubspec.yaml'); |
| if (pubspec.existsSync()) { |
| if (pubspec.readAsStringSync().contains('name: objective_c\n') || |
| pubspec.readAsStringSync().contains('name: objective_c\r\n')) { |
| return dir.uri; |
| } |
| } |
| dir = dir.parent; |
| } |
| } |
| return Directory.current.uri; |
| } |
| |
| void dartCmd(List<String> args, {String? workingDir}) { |
| final exec = Platform.resolvedExecutable; |
| final proc = Process.runSync( |
| exec, |
| args, |
| runInShell: true, |
| workingDirectory: workingDir, |
| ); |
| if (proc.exitCode != 0) { |
| exitCode = proc.exitCode; |
| print(proc.stdout); |
| print(proc.stderr); |
| throw Exception('Command failed: $exec ${args.join(" ")}'); |
| } |
| } |
| |
| final _clsDecl = RegExp(r'^extension type (\w+)\W'); |
| String? parseClassDecl(String line) => _clsDecl.firstMatch(line)?[1]; |
| |
| Map<String, String> parseExtraMethods(String filename) { |
| final extraMethods = <String, String>{}; |
| String? currentClass; |
| late StringBuffer methods; |
| for (final line in File(filename).readAsLinesSync()) { |
| if (currentClass == null) { |
| final cls = parseClassDecl(line); |
| if (cls != null) { |
| currentClass = cls; |
| methods = StringBuffer(); |
| } |
| } else { |
| if (line == '}') { |
| extraMethods[currentClass] = methods.toString(); |
| currentClass = null; |
| } else { |
| methods.writeln(line); |
| } |
| } |
| } |
| return extraMethods; |
| } |
| |
| void mergeExtraMethods(String filename, Map<String, String> extraMethods) { |
| final out = StringBuffer(); |
| for (final line in File(filename).readAsLinesSync()) { |
| out.writeln(line); |
| final cls = parseClassDecl(line); |
| final extra = cls == null ? null : extraMethods[cls]; |
| if (cls != null && extra != null) { |
| out.writeln(extra); |
| extraMethods.remove(cls); |
| } |
| } |
| assert(extraMethods.isEmpty); |
| |
| File(filename).writeAsStringSync(out.toString()); |
| } |