| // 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. |
| |
| // Runs the ffigen configs, then merges tool/data/extra_methods.dart.in into the |
| // Objective C bindings. |
| |
| // ignore_for_file: avoid_print |
| |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:ffigen/src/executables/ffigen.dart' as ffigen; |
| import 'package:yaml/yaml.dart'; |
| |
| const cConfig = 'ffigen_c.yaml'; |
| const objcConfig = 'ffigen_objc.yaml'; |
| const cBindings = 'lib/src/c_bindings_generated.dart'; |
| const objcBindings = 'lib/src/objective_c_bindings_generated.dart'; |
| const objcExports = 'lib/src/objective_c_bindings_exported.dart'; |
| const extraMethodsFile = 'tool/data/extra_methods.dart.in'; |
| const builtInTypes = |
| '../ffigen/lib/src/code_generator/objc_built_in_types.dart'; |
| const interfaceListTest = 'test/interface_lists_test.dart'; |
| |
| const privateInterfaces = <String>{ |
| 'DartInputStreamAdapter', |
| 'DartInputStreamAdapterWeakHolder', |
| 'DOBJCObservation', |
| }; |
| |
| final privateMethods = <String>{ |
| for (final name in privateInterfaces) '$name\$Methods', |
| }; |
| final privateClasses = privateInterfaces.union(privateMethods); |
| |
| void dartCmd(List<String> args) { |
| final exec = Platform.resolvedExecutable; |
| final proc = Process.runSync(exec, args, runInShell: true); |
| if (proc.exitCode != 0) { |
| exitCode = proc.exitCode; |
| print(proc.stdout); |
| print(proc.stderr); |
| throw Exception('Command failed: $exec ${args.join(" ")}'); |
| } |
| } |
| |
| typedef ClassInfo = ({ |
| String name, |
| String? ext, |
| List<String> mix, |
| List<String> impl, |
| }); |
| final _clsDecl = RegExp( |
| r'^class (.*?)(?: extends (.*?))?(?: with (.*?))?(?: implements (.*?))? {', |
| ); |
| ClassInfo? parseClassDecl(String line) { |
| final match = _clsDecl.firstMatch(line); |
| if (match == null) return null; |
| return ( |
| name: match[1]!, |
| ext: match[2], |
| mix: match[3]?.split(', ') ?? [], |
| impl: match[4]?.split(', ') ?? [], |
| ); |
| } |
| |
| typedef ExtraMethods = ({ClassInfo cls, String methods}); |
| Map<String, ExtraMethods> parseExtraMethods(String filename) { |
| final extraMethods = <String, ExtraMethods>{}; |
| ClassInfo? 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.name] = ( |
| cls: currentClass, |
| methods: methods.toString(), |
| ); |
| currentClass = null; |
| } else { |
| methods.writeln(line); |
| } |
| } |
| } |
| return extraMethods; |
| } |
| |
| String classDecl( |
| String name, |
| String? ext, |
| List<String> mix, |
| List<String> impl, |
| ) => [ |
| 'class $name', |
| if (ext != null) 'extends $ext', |
| if (mix.isNotEmpty) 'with ${mix.join(', ')}', |
| if (impl.isNotEmpty) 'implements ${impl.join(', ')}', |
| '{', |
| ].join(' '); |
| |
| void mergeExtraMethods( |
| String filename, |
| Map<String, ExtraMethods> extraMethods, |
| ) { |
| final out = StringBuffer(); |
| for (final line in File(filename).readAsLinesSync()) { |
| final cls = parseClassDecl(line); |
| final extra = cls == null ? null : extraMethods[cls.name]; |
| if (cls == null || extra == null) { |
| out.writeln(line); |
| } else { |
| out.writeln( |
| classDecl( |
| cls.name, |
| extra.cls.ext ?? cls.ext, |
| [...cls.mix, ...extra.cls.mix], |
| [...cls.impl, ...extra.cls.impl], |
| ), |
| ); |
| out.writeln(extra.methods); |
| extraMethods.remove(cls.name); |
| } |
| } |
| |
| // Matching classes have been removed from extraMethods. Write all the |
| // remaining classes separately. |
| for (final extra in extraMethods.values) { |
| out.writeln('\n'); |
| out.writeln( |
| classDecl(extra.cls.name, extra.cls.ext, extra.cls.mix, extra.cls.impl), |
| ); |
| out.writeln(extra.methods); |
| out.writeln('}'); |
| } |
| |
| File(filename).writeAsStringSync(out.toString()); |
| } |
| |
| List<String> writeBuiltInTypes(String config, String out) { |
| final yaml = loadYaml(File(config).readAsStringSync()) as YamlMap; |
| |
| 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/generate_code.dart. |
| '''); |
| |
| Iterable<String> writeDecls(String name, String key) { |
| final decls = yaml[key] as YamlMap; |
| final renames = decls['rename'] as YamlMap? ?? YamlMap(); |
| final includes = decls['include'] as YamlList; |
| |
| final names = <String, String>{ |
| for (final inc in includes.map<String>((i) => i as String)) |
| inc: renames[inc] as String? ?? inc, |
| }; |
| exports.addAll(names.values); |
| final anyRenames = names.entries.any((kv) => kv.key != kv.value); |
| final elements = anyRenames |
| ? names.entries.map((kv) => " '${kv.key}': '${kv.value}',") |
| : names.keys.map((key) => " '$key',"); |
| |
| s.write(''' |
| |
| const $name = { |
| ${elements.join('\n')} |
| }; |
| '''); |
| return names.values; |
| } |
| |
| final interfaces = writeDecls('objCBuiltInInterfaces', 'objc-interfaces'); |
| exports.addAll([for (final name in interfaces) '$name\$Methods']); |
| writeDecls('objCBuiltInCompounds', 'structs'); |
| writeDecls('objCBuiltInEnums', 'enums'); |
| writeDecls('objCBuiltInProtocols', 'objc-protocols'); |
| writeDecls('objCBuiltInCategories', 'objc-categories'); |
| writeDecls('objCBuiltInGlobals', 'globals'); |
| |
| File(out).writeAsStringSync(s.toString()); |
| |
| return exports.difference(privateClasses).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/generate_code.dart. |
| |
| export 'objective_c_bindings_generated.dart' |
| show |
| ${exports.join(',\n ')}; |
| '''); |
| } |
| |
| Future<void> run({required bool format}) async { |
| print('Generating C bindings...'); |
| await ffigen.main(['--no-format', '-v', 'severe', '--config', cConfig]); |
| |
| print('Generating ObjC bindings...'); |
| await ffigen.main(['--no-format', '-v', 'severe', '--config', objcConfig]); |
| mergeExtraMethods(objcBindings, parseExtraMethods(extraMethodsFile)); |
| |
| print('Generating objc_built_in_types.dart...'); |
| final exports = writeBuiltInTypes(objcConfig, builtInTypes); |
| |
| print('Generating objective_c_bindings_exported.dart...'); |
| writeExports(exports, objcExports); |
| |
| if (format) { |
| print('Formatting bindings...'); |
| dartCmd(['format', cBindings, objcBindings, builtInTypes, objcExports]); |
| } |
| |
| print('Running tests...'); |
| dartCmd(['test', interfaceListTest]); |
| } |
| |
| Future<void> main(List<String> args) async { |
| Directory.current = Platform.script.resolve('..').path; |
| final argResults = |
| (ArgParser()..addFlag( |
| 'format', |
| help: 'Format the generated code.', |
| defaultsTo: true, |
| negatable: true, |
| )) |
| .parse(args); |
| await run(format: argResults.flag('format')); |
| } |