blob: ae6c0866f61eded32936a49cb4c3ab1f25ad1513 [file]
// 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.
// Re-compile trigger.
// Runs the FFIgen visitors, then merges tool/data/extra_methods.dart.in into the
// Objective C bindings.
import 'dart:io';
import 'dart:isolate';
import 'package:args/args.dart';
import 'package:ffigen/ffigen.dart';
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 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';
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(" ")}');
}
}
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();
String? pendingClass;
String? pendingExtra;
for (final line in File(filename).readAsLinesSync()) {
out.writeln(line);
if (pendingClass != null) {
if (line.contains('{')) {
out.writeln(pendingExtra);
pendingClass = null;
pendingExtra = null;
}
} else {
final cls = parseClassDecl(line);
final extra = cls == null ? null : extraMethods[cls];
if (cls != null && extra != null) {
if (line.contains('{')) {
out.writeln(extra);
extraMethods.remove(cls);
} else {
pendingClass = cls;
pendingExtra = extra;
extraMethods.remove(cls);
}
}
}
}
assert(extraMethods.isEmpty);
File(filename).writeAsStringSync(out.toString());
}
String renameRuntimeFunction(String name) {
const custom = {
'objc_retain': 'objectRetain',
'objc_retainBlock': 'blockRetain',
'objc_release': 'objectRelease',
'objc_autorelease': 'objectAutorelease',
'objc_msgSend_fpret': 'msgSendFpret',
'objc_msgSend_stret': 'msgSendStret',
'object_getClass': 'getObjectClass',
'protocol_getName': 'getProtocolName',
};
if (custom.containsKey(name)) return custom[name]!;
for (final prefix in ['sel_', 'objc_', 'protocol_', 'object_']) {
if (name.startsWith(prefix)) {
return name.substring(prefix.length);
}
}
return name;
}
String renameInterface(String name) =>
name.startsWith('DOBJCDart') ? name.substring(5) : name;
String renameProtocol(String name) =>
name == 'NSObject' ? 'NSObjectProtocol' : name;
String renameStruct(String name) => name.startsWith('__')
? name.substring(2)
: (name.startsWith('_') ? name.substring(1) : name);
void generateRuntimeBindings(Uri root) {
const functions = {
'object_getClass',
'sel_registerName',
'sel_getName',
'protocol_getMethodDescription',
'protocol_getName',
};
const globals = {
'NSKeyValueChangeIndexesKey',
'NSKeyValueChangeKindKey',
'NSKeyValueChangeNewKey',
'NSKeyValueChangeNotificationIsPriorKey',
'NSKeyValueChangeOldKey',
'NSLocalizedDescriptionKey',
};
FfiGenerator(
input: Input(entryPoints: [root.resolve('src/objective_c_runtime.h')]),
visitors: [
Visitor.callback(
visitFunc: (node) {
final isObjc = node.originalName.startsWith('objc_');
if (!isObjc && !functions.contains(node.originalName)) {
node.isIncluded = false;
return;
}
node.isIncluded = true;
if (!node.originalName.startsWith('objc_msgSend')) {
node.isLeaf = true;
}
node.name = renameRuntimeFunction(node.originalName);
},
visitGlobal: (node) {
if (node.originalName.startsWith('_') &&
node.originalName.endsWith('Block')) {
node.isIncluded = true;
node.name = node.originalName.substring(1);
} else if (globals.contains(node.originalName)) {
node.isIncluded = true;
if (node.originalName.startsWith('_')) {
node.name = node.originalName.substring(1);
}
} else {
node.isIncluded = false;
}
},
visitStruct: (node) {
if (node.originalName.startsWith('_ObjC')) {
node.isIncluded = true;
node.name = 'ObjC${node.originalName.substring(5)}';
}
},
visitEnum: (node) => node.isIncluded = false,
visitMacroConstant: (node) => node.isIncluded = false,
visitUnnamedEnumConstant: (node) => node.isIncluded = false,
visitUnion: (node) => node.isIncluded = false,
),
],
output: Output(
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/generate_code.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
''',
style: const NativeExternalBindings(),
dartFile: root.resolve(runtimeBindings),
),
).generate();
}
void generateCBindings(Uri root) {
const structs = {'_ObjCBlockDesc', '_ObjCBlockImpl', '_ObjCObjectImpl'};
const nonLeaf = {
'DOBJC_deleteFinalizableHandle',
'DOBJC_disposeObjCBlockWithClosure',
'DOBJC_newFinalizableBool',
'DOBJC_newFinalizableHandle',
'DOBJC_awaitWaiter',
};
FfiGenerator(
input: Input(
entryPoints: [
root.resolve('src/include/dart_api_dl.h'),
root.resolve('src/objective_c.h'),
root.resolve('src/os_version.h'),
],
),
visitors: [
Visitor.callback(
visitFunc: (node) {
final isDobjc = node.originalName.startsWith('DOBJC_');
final isNewFinalizable = node.originalName == 'newFinalizableHandle';
if (!isDobjc && !isNewFinalizable) {
node.isIncluded = false;
return;
}
node.isIncluded = true;
if (!nonLeaf.contains(node.originalName)) {
node.isLeaf = true;
}
if (isDobjc) {
node.name = node.originalName.substring(6);
}
},
visitTypealias: (node) {
if (node.originalName == 'Dart_FinalizableHandle') {
node.isIncluded = true;
}
},
visitStruct: (node) {
if (node.originalName == '_DOBJC_Context') {
node.isIncluded = true;
node.name = 'DOBJC_Context';
} else if (node.originalName == '_Dart_FinalizableHandle') {
node.isIncluded = true;
node.name = 'Dart_FinalizableHandle_';
} else if (structs.contains(node.originalName)) {
node.isIncluded = true;
node.name = 'ObjC${node.originalName.substring(5)}';
} else {
node.isIncluded = false;
}
},
visitMacroConstant: (node) {
if (node.originalName == 'ILLEGAL_PORT') {
node.isIncluded = true;
} else {
node.isIncluded = false;
}
},
visitEnum: (node) => node.isIncluded = false,
visitGlobal: (node) => node.isIncluded = false,
visitUnnamedEnumConstant: (node) => node.isIncluded = false,
visitUnion: (node) => node.isIncluded = false,
),
],
output: Output(
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/generate_code.dart`.
// coverage:ignore-file
''',
style: const NativeExternalBindings(
assetId: 'package:objective_c/objective_c.dylib',
),
dartFile: root.resolve(cBindings),
),
).generate();
}
void generateObjCBindings(Uri root) {
const interfaces = {
'DOBJCDartInputStreamAdapter',
'DOBJCDartInputStreamAdapterWeakHolder',
'DOBJCObservation',
'DOBJCDartProtocolBuilder',
'DOBJCDartProtocol',
'NSArray',
'NSAttributedString',
'NSAttributedStringMarkdownParsingOptions',
'NSBundle',
'NSCharacterSet',
'NSCoder',
'NSData',
'NSDate',
'NSDictionary',
'NSEnumerator',
'NSError',
'NSIndexSet',
'NSInputStream',
'NSInvocation',
'NSItemProvider',
'NSLocale',
'NSMethodSignature',
'NSMutableArray',
'NSMutableData',
'NSMutableDictionary',
'NSMutableIndexSet',
'NSMutableOrderedSet',
'NSMutableSet',
'NSMutableString',
'NSNotification',
'NSNull',
'NSNumber',
'NSObject',
'NSOutputStream',
'NSOrderedCollectionChange',
'NSOrderedCollectionDifference',
'NSOrderedSet',
'NSPort',
'NSPortMessage',
'NSProgress',
'NSRunLoop',
'NSSet',
'NSStream',
'NSString',
'NSTimer',
'NSURL',
'NSURLHandle',
'NSValue',
'Protocol',
};
const protocols = {
'NSCoding',
'NSCopying',
'NSFastEnumeration',
'NSItemProviderReading',
'NSItemProviderWriting',
'NSMutableCopying',
'NSObject',
'NSPortDelegate',
'NSSecureCoding',
'NSStreamDelegate',
'NSURLHandleClient',
'Observer',
};
const categories = {
'NSDataCreation',
'NSExtendedArray',
'NSExtendedData',
'NSExtendedDate',
'NSExtendedDictionary',
'NSExtendedEnumerator',
'NSExtendedMutableArray',
'NSExtendedMutableData',
'NSExtendedMutableDictionary',
'NSExtendedMutableOrderedSet',
'NSExtendedMutableSet',
'NSExtendedOrderedSet',
'NSExtendedSet',
'NSNumberCreation',
'NSNumberIsFloat',
'NSNumberIsBool',
'NSStringExtensionMethods',
};
const structs = {
'AEDesc',
'__CFRunLoop',
'__CFString',
'CGPoint',
'_CGPoint',
'CGRect',
'_CGRect',
'CGSize',
'_CGSize',
'NSEdgeInsets',
'_NSEdgeInsets',
'NSFastEnumerationState',
'_NSFastEnumerationState',
'_NSRange',
'NSRange',
'_NSZone',
'NSZone',
'OpaqueAEDataStorageType',
};
const enums = {
'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',
};
FfiGenerator(
input: Input(
entryPoints: [
root.resolve('src/foundation.h'),
root.resolve('src/input_stream_adapter.h'),
root.resolve('src/ns_number.h'),
root.resolve('src/observer.h'),
root.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.callback(
visitFunc: (node) => node.isIncluded = false,
visitObjCInterface: (node) {
node.includeCategories = false;
if (interfaces.contains(node.originalName)) {
node.isIncluded = true;
node.name = renameInterface(node.originalName);
} else {
node.isIncluded = false;
}
if (node.originalName == 'NSBundle') {
for (final method in node.methods) {
if (method.originalName ==
'localizedStringForKey:value:table:localizations:') {
method.isIncluded = false;
}
}
}
},
visitObjCProtocol: (node) {
if (protocols.contains(node.originalName)) {
node.isIncluded = true;
node.name = renameProtocol(node.originalName);
} else {
node.isIncluded = false;
}
},
visitObjCCategory: (node) {
node.isIncluded = categories.contains(node.originalName);
},
visitStruct: (node) {
node.dependencies = CompoundDependencies.opaque;
if (node.originalName.isNotEmpty &&
structs.contains(node.originalName)) {
node.isIncluded = true;
node.name = renameStruct(node.originalName);
} else {
node.isIncluded = false;
}
},
visitEnum: (node) {
node.isIncluded = enums.contains(node.originalName);
},
visitTypealias: (node) {
if (node.originalName == 'CFStringRef') {
node.isIncluded = true;
} else {
node.isIncluded = false;
}
},
visitGlobal: (node) => node.isIncluded = false,
visitMacroConstant: (node) => node.isIncluded = false,
visitUnnamedEnumConstant: (node) => node.isIncluded = false,
visitUnion: (node) => node.isIncluded = false,
),
],
output: Output(
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/generate_code.dart`.
// coverage:ignore-file
''',
format: false,
style: const NativeExternalBindings(
assetId: 'package:objective_c/objective_c.dylib',
),
dartFile: root.resolve(objcBindings),
objectiveCFile: root.resolve('src/objective_c_bindings_generated.m'),
),
).generate();
mergeExtraMethods(objcBindings, parseExtraMethods(extraMethodsFile));
print('Generating objc_built_in_types.dart...');
final exports = writeBuiltInTypes(
builtInTypes,
objcBindings,
interfaces: interfaces,
structs: structs,
protocols: protocols,
);
print('Generating objc_bindings_exported.dart...');
writeExports(exports, objcExports);
}
List<String> writeBuiltInTypes(
String out,
String bindingsFile, {
required Set<String> interfaces,
required Set<String> structs,
required Set<String> protocols,
}) {
final bindingsLines = File(bindingsFile).readAsLinesSync();
Set<String> findBindings(RegExp re) => bindingsLines
.map(re.firstMatch)
.nonNulls
.map((match) => match[1]!)
.toSet();
final genInterfaces = findBindings(
RegExp(r'^extension type ([^_]\w*)\._\( *objc\.ObjCObject '),
).toList()..sort();
final genStructs = findBindings(
RegExp(r'^final class (\w+) extends ffi\.(Struct|Opaque)'),
).toList()..sort();
final genEnums = findBindings(
RegExp(r'^(?:enum|sealed class) (\w+) {'),
).toList()..sort();
final genProtocols = findBindings(
RegExp(r'^extension type ([^_]\w*)\._\(objc\.ObjCProtocol '),
).toList()..sort();
final genCategories = findBindings(
RegExp(r'^extension (\w+) on \w+ {'),
).toList()..sort();
final genAllExtensions = findBindings(
RegExp(r'^extension (?!type\b)([\w\$]+)'),
).toSet();
final interfacesMap = {
for (final name in genInterfaces)
interfaces.firstWhere(
(i) => renameInterface(i) == name,
orElse: () => name,
): name,
};
final structsMap = {
for (final name in genStructs)
structs.firstWhere((s) => renameStruct(s) == name, orElse: () => name):
name,
};
final protocolsMap = {
for (final name in genProtocols)
protocols.firstWhere(
(p) => renameProtocol(p) == name,
orElse: () => name,
): name,
};
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.
''');
void writeDecls(
String name,
Map<String, String> namesMap, [
Iterable<String>? namesIterable,
]) {
final keys = namesIterable ?? namesMap.keys;
final map = namesIterable != null
? {for (final k in keys) k: k}
: Map<String, String>.from(namesMap);
exports.addAll(map.values);
final anyRenames = map.entries.any((kv) => kv.key != kv.value);
final elements = anyRenames
? map.entries.map(
(kv) =>
" '${kv.key.replaceAll(r'$', r'\$')}': "
"'${kv.value.replaceAll(r'$', r'\$')}',",
)
: map.keys.map((key) => " '${key.replaceAll(r'$', r'\$')}',");
s.write('''
const $name = {
${elements.join('\n')}
};
''');
}
writeDecls('objCBuiltInInterfaces', interfacesMap);
exports.addAll([
for (final name in interfacesMap.values)
if (genAllExtensions.contains('$name\$Methods')) '$name\$Methods',
]);
writeDecls('objCBuiltInCompounds', structsMap);
writeDecls('objCBuiltInEnums', {}, genEnums);
writeDecls('objCBuiltInProtocols', protocolsMap);
exports.addAll([
for (final name in protocolsMap.values)
if (genAllExtensions.contains('$name\$Methods')) '$name\$Methods',
]);
exports.addAll([for (final name in protocolsMap.values) '$name\$Builder']);
writeDecls('objCBuiltInCategories', {}, genCategories);
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/generate_code.dart.
export 'objective_c_bindings_generated.dart'
show
${exports.join(',\n ')};
''');
}
Future<void> run({required bool format}) async {
final pkgUri = await Isolate.resolvePackageUri(
Uri.parse('package:objective_c/objective_c.dart'),
);
final root = (pkgUri ?? Platform.script).resolve('../');
print('Generating runtime bindings...');
generateRuntimeBindings(root);
print('Generating C bindings...');
generateCBindings(root);
print('Generating ObjC bindings...');
generateObjCBindings(root);
if (format) {
print('Formatting bindings...');
dartCmd([
'format',
runtimeBindings,
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'));
}