blob: e3b75dd9468fa919ea967d0dfcaa80d04f02295a [file] [log] [blame]
// Copyright (c) 2022, 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:ffigen/src/code_generator.dart';
import 'binding_string.dart';
import 'writer.dart';
/// Built in functions used by the Objective C bindings.
class ObjCBuiltInFunctions {
late final _registerNameFunc = Func(
name: '_sel_registerName',
originalName: 'sel_registerName',
returnType: PointerType(objCSelType),
parameters: [Parameter(name: 'str', type: PointerType(charType))],
isInternal: true,
);
late final registerName = ObjCInternalFunction(
'_registerName', _registerNameFunc, (Writer w, String name) {
final selType = _registerNameFunc.functionType.returnType.getCType(w);
return '''
$selType $name(String name) {
final cstr = name.toNativeUtf8();
final sel = ${_registerNameFunc.name}(cstr.cast());
${w.ffiPkgLibraryPrefix}.calloc.free(cstr);
return sel;
}
''';
});
late final _getClassFunc = Func(
name: '_objc_getClass',
originalName: 'objc_getClass',
returnType: PointerType(objCObjectType),
parameters: [Parameter(name: 'str', type: PointerType(charType))],
isInternal: true,
);
late final getClass =
ObjCInternalFunction('_getClass', _getClassFunc, (Writer w, String name) {
final objType = _getClassFunc.functionType.returnType.getCType(w);
return '''
$objType $name(String name) {
final cstr = name.toNativeUtf8();
final clazz = ${_getClassFunc.name}(cstr.cast());
${w.ffiPkgLibraryPrefix}.calloc.free(cstr);
if (clazz == ${w.ffiLibraryPrefix}.nullptr) {
throw Exception('Failed to load Objective-C class: \$name');
}
return clazz;
}
''';
});
late final _retainFunc = Func(
name: '_objc_retain',
originalName: 'objc_retain',
returnType: PointerType(objCObjectType),
parameters: [Parameter(name: 'value', type: PointerType(objCObjectType))],
isInternal: true,
);
late final _releaseFunc = Func(
name: '_objc_release',
originalName: 'objc_release',
returnType: voidType,
parameters: [Parameter(name: 'value', type: PointerType(objCObjectType))],
isInternal: true,
);
late final _releaseFinalizer = ObjCInternalGlobal(
'_objc_releaseFinalizer',
(Writer w) => '${w.ffiLibraryPrefix}.NativeFinalizer('
'${_releaseFunc.funcPointerName}.cast())',
_releaseFunc,
);
late final _blockCopyFunc = Func(
name: '_Block_copy',
originalName: '_Block_copy',
returnType: PointerType(voidType),
parameters: [Parameter(name: 'value', type: PointerType(voidType))],
isInternal: true,
);
late final _blockReleaseFunc = Func(
name: '_Block_release',
originalName: '_Block_release',
returnType: voidType,
parameters: [Parameter(name: 'value', type: PointerType(voidType))],
isInternal: true,
);
late final _blockReleaseFinalizer = ObjCInternalGlobal(
'_objc_releaseFinalizer',
(Writer w) => '${w.ffiLibraryPrefix}.NativeFinalizer('
'${_blockReleaseFunc.funcPointerName}.cast())',
_blockReleaseFunc,
);
// We need to load a separate instance of objc_msgSend for each signature. If
// the return type is a struct, we need to use objc_msgSend_stret instead, and
// for float return types we need objc_msgSend_fpret.
final _msgSendFuncs = <String, ObjCMsgSendFunc>{};
ObjCMsgSendFunc getMsgSendFunc(
Type returnType, List<ObjCMethodParam> params) {
var key = returnType.cacheKey();
for (final p in params) {
key += ' ${p.type.cacheKey()}';
}
return _msgSendFuncs[key] ??= ObjCMsgSendFunc(
'_objc_msgSend_${_msgSendFuncs.length}',
returnType,
params,
_msgSendUseVariants);
}
late final _msgSendUseVariants = ObjCInternalGlobal(
'_objc_msgSend_useVariants',
(Writer w) => '''
${w.ffiLibraryPrefix}.Abi.current() == ${w.ffiLibraryPrefix}.Abi.iosX64 ||
${w.ffiLibraryPrefix}.Abi.current() == ${w.ffiLibraryPrefix}.Abi.macosX64
''');
final _selObjects = <String, ObjCInternalGlobal>{};
ObjCInternalGlobal getSelObject(String methodName) {
return _selObjects[methodName] ??= ObjCInternalGlobal(
'_sel_${methodName.replaceAll(":", "_")}',
(Writer w) => '${registerName.name}("$methodName")',
registerName,
);
}
// See https://clang.llvm.org/docs/Block-ABI-Apple.html
late final blockStruct = Struct(
name: '_ObjCBlock',
isInternal: true,
members: [
Member(name: 'isa', type: PointerType(voidType)),
Member(name: 'flags', type: intType),
Member(name: 'reserved', type: intType),
Member(name: 'invoke', type: PointerType(voidType)),
Member(name: 'descriptor', type: PointerType(blockDescStruct)),
Member(name: 'target', type: PointerType(voidType)),
],
);
late final blockDescStruct = Struct(
name: '_ObjCBlockDesc',
isInternal: true,
members: [
Member(name: 'reserved', type: unsignedLongType),
Member(name: 'size', type: unsignedLongType),
Member(name: 'copy_helper', type: PointerType(voidType)),
Member(name: 'dispose_helper', type: PointerType(voidType)),
Member(name: 'signature', type: PointerType(charType)),
],
);
late final newBlockDesc =
ObjCInternalFunction('_newBlockDesc', null, (Writer w, String name) {
final blockType = blockStruct.getCType(w);
final descType = blockDescStruct.getCType(w);
final descPtr = PointerType(blockDescStruct).getCType(w);
return '''
$descPtr $name() {
final d = ${w.ffiPkgLibraryPrefix}.calloc.allocate<$descType>(
${w.ffiLibraryPrefix}.sizeOf<$descType>());
d.ref.reserved = 0;
d.ref.size = ${w.ffiLibraryPrefix}.sizeOf<$blockType>();
d.ref.copy_helper = ${w.ffiLibraryPrefix}.nullptr;
d.ref.dispose_helper = ${w.ffiLibraryPrefix}.nullptr;
d.ref.signature = ${w.ffiLibraryPrefix}.nullptr;
return d;
}
''';
});
late final blockDescSingleton = ObjCInternalGlobal(
'_objc_block_desc',
(Writer w) => '${newBlockDesc.name}()',
blockDescStruct,
);
late final concreteGlobalBlock = ObjCInternalGlobal(
'_objc_concrete_global_block',
(Writer w) => '${w.lookupFuncIdentifier}<${voidType.getCType(w)}>('
"'_NSConcreteGlobalBlock')",
);
late final newBlock = ObjCInternalFunction('_newBlock', _blockCopyFunc,
(Writer w, String name) {
final blockType = blockStruct.getCType(w);
final blockPtr = PointerType(blockStruct).getCType(w);
final voidPtr = PointerType(voidType).getCType(w);
return '''
$blockPtr $name($voidPtr invoke, $voidPtr target) {
final b = ${w.ffiPkgLibraryPrefix}.calloc.allocate<$blockType>(
${w.ffiLibraryPrefix}.sizeOf<$blockType>());
b.ref.isa = ${concreteGlobalBlock.name};
b.ref.flags = 0;
b.ref.reserved = 0;
b.ref.invoke = invoke;
b.ref.target = target;
b.ref.descriptor = ${blockDescSingleton.name};
final copy = ${_blockCopyFunc.name}(b.cast()).cast<$blockType>();
${w.ffiPkgLibraryPrefix}.calloc.free(b);
return copy;
}
''';
});
void _writeFinalizableClass(
Writer w,
StringBuffer s,
String name,
String kind,
String idType,
String retain,
String release,
String finalizer) {
s.write('''
class $name implements ${w.ffiLibraryPrefix}.Finalizable {
final $idType _id;
final ${w.className} _lib;
bool _pendingRelease;
$name._(this._id, this._lib,
{bool retain = false, bool release = false}) : _pendingRelease = release {
if (retain) {
_lib.$retain(_id.cast());
}
if (release) {
_lib.$finalizer.attach(this, _id.cast(), detach: this);
}
}
/// Releases the reference to the underlying ObjC $kind held by this wrapper.
/// Throws a StateError if this wrapper doesn't currently hold a reference.
void release() {
if (_pendingRelease) {
_pendingRelease = false;
_lib.$release(_id.cast());
_lib.$finalizer.detach(this);
} else {
throw StateError(
'Released an ObjC $kind that was unowned or already released.');
}
}
@override
bool operator ==(Object other) {
return other is $name && _id == other._id;
}
@override
int get hashCode => _id.hashCode;
/// Return a pointer to this object.
$idType get pointer => _id;
/// Retain a reference to this object and then return the pointer. This
/// reference must be released when you are done with it. If you wrap this
/// reference in another object, make sure to release it but not retain it:
/// `castFromPointer(lib, pointer, retain: false, release: true)`
$idType retainAndReturnPointer() {
_lib.$retain(_id.cast());
return _id;
}
}
''');
}
bool utilsExist = false;
void ensureUtilsExist(Writer w, StringBuffer s) {
if (utilsExist) return;
utilsExist = true;
_writeFinalizableClass(
w,
s,
'_ObjCWrapper',
'object',
PointerType(objCObjectType).getCType(w),
_retainFunc.name,
_releaseFunc.name,
_releaseFinalizer.name);
}
bool blockUtilsExist = false;
void ensureBlockUtilsExist(Writer w, StringBuffer s) {
if (blockUtilsExist) return;
blockUtilsExist = true;
_writeFinalizableClass(
w,
s,
'_ObjCBlockBase',
'block',
PointerType(blockStruct).getCType(w),
_blockCopyFunc.name,
_blockReleaseFunc.name,
_blockReleaseFinalizer.name);
}
void addDependencies(Set<Binding> dependencies) {
registerName.addDependencies(dependencies);
getClass.addDependencies(dependencies);
_retainFunc.addDependencies(dependencies);
_releaseFunc.addDependencies(dependencies);
_releaseFinalizer.addDependencies(dependencies);
for (final msgSendFunc in _msgSendFuncs.values) {
msgSendFunc.addDependencies(dependencies);
}
for (final sel in _selObjects.values) {
sel.addDependencies(dependencies);
}
}
void addBlockDependencies(Set<Binding> dependencies) {
newBlockDesc.addDependencies(dependencies);
blockDescSingleton.addDependencies(dependencies);
blockStruct.addDependencies(dependencies);
concreteGlobalBlock.addDependencies(dependencies);
newBlock.addDependencies(dependencies);
_blockCopyFunc.addDependencies(dependencies);
_blockReleaseFunc.addDependencies(dependencies);
_blockReleaseFinalizer.addDependencies(dependencies);
}
final _interfaceRegistry = <String, ObjCInterface>{};
void registerInterface(ObjCInterface interface) {
_interfaceRegistry[interface.originalName] = interface;
}
ObjCInterface get nsData {
return _interfaceRegistry["NSData"] ??
(ObjCInterface(
originalName: "NSData",
builtInFunctions: this,
));
}
void generateNSStringUtils(Writer w, StringBuffer s) {
// Generate a constructor that wraps stringWithCharacters, and a toString
// method that wraps dataUsingEncoding.
s.write('''
factory NSString(${w.className} _lib, String str) {
final cstr = str.toNativeUtf16();
final nsstr = stringWithCharacters_length_(_lib, cstr.cast(), str.length);
${w.ffiPkgLibraryPrefix}.calloc.free(cstr);
return nsstr;
}
@override
String toString() {
final data = dataUsingEncoding_(
0x94000100 /* NSUTF16LittleEndianStringEncoding */);
return data!.bytes.cast<${w.ffiPkgLibraryPrefix}.Utf16>().toDartString(
length: length);
}
''');
}
void generateStringUtils(Writer w, StringBuffer s) {
// Generate an extension on String to convert to NSString
s.write('''
extension StringToNSString on String {
NSString toNSString(${w.className} lib) => NSString(lib, this);
}
''');
}
}
/// Functions only used internally by ObjC bindings, which may or may not wrap a
/// native function, such as getClass.
class ObjCInternalFunction extends LookUpBinding {
final Func? _wrappedFunction;
final String Function(Writer, String) _toBindingString;
ObjCInternalFunction(
String name, this._wrappedFunction, this._toBindingString)
: super(originalName: name, name: name, isInternal: true);
@override
BindingString toBindingString(Writer w) {
name = w.wrapperLevelUniqueNamer.makeUnique(name);
return BindingString(
type: BindingStringType.func, string: _toBindingString(w, name));
}
@override
void addDependencies(Set<Binding> dependencies) {
if (dependencies.contains(this)) return;
dependencies.add(this);
_wrappedFunction?.addDependencies(dependencies);
}
}
/// Globals only used internally by ObjC bindings, such as classes and SELs.
class ObjCInternalGlobal extends LookUpBinding {
final String Function(Writer) makeValue;
Binding? binding;
ObjCInternalGlobal(String name, this.makeValue, [this.binding])
: super(originalName: name, name: name, isInternal: true);
@override
BindingString toBindingString(Writer w) {
final s = StringBuffer();
name = w.wrapperLevelUniqueNamer.makeUnique(name);
s.write('late final $name = ${makeValue(w)};');
return BindingString(type: BindingStringType.global, string: s.toString());
}
@override
void addDependencies(Set<Binding> dependencies) {
if (dependencies.contains(this)) return;
dependencies.add(this);
binding?.addDependencies(dependencies);
}
}
enum ObjCMsgSendVariant {
normal('objc_msgSend'),
stret('objc_msgSend_stret'),
fpret('objc_msgSend_fpret');
final String name;
const ObjCMsgSendVariant(this.name);
static ObjCMsgSendVariant fromReturnType(Type returnType) {
if (returnType is Compound && returnType.isStruct) {
return ObjCMsgSendVariant.stret;
} else if (returnType == floatType || returnType == doubleType) {
return ObjCMsgSendVariant.fpret;
}
return ObjCMsgSendVariant.normal;
}
}
/// A wrapper around the objc_msgSend function, or the stret or fpret variants.
///
/// The [variant] is based purely on the return type of the method.
///
/// For the stret and fpret variants, we may need to fall back to the normal
/// objc_msgSend function at runtime, depending on the ABI. So we emit both the
/// variant function and the normal function, and decide which to use at runtime
/// based on the ABI. The result of the ABI check is stored in [useVariants].
///
/// This runtime check is complicated by the fact that objc_msgSend_stret has
/// a different signature than objc_msgSend has for the same method. This is
/// because objc_msgSend_stret takes a pointer to the return type as its first
/// arg.
class ObjCMsgSendFunc {
final ObjCMsgSendVariant variant;
final ObjCInternalGlobal useVariants;
// [normalFunc] is always a reference to the normal objc_msgSend function. If
// the [variant] is fpret or stret, then [variantFunc] is a reference to the
// corresponding variant of the objc_msgSend function, otherwise it's null.
late final Func normalFunc;
late final Func? variantFunc;
ObjCMsgSendFunc(String name, Type returnType, List<ObjCMethodParam> params,
this.useVariants)
: variant = ObjCMsgSendVariant.fromReturnType(returnType) {
normalFunc = Func(
name: name,
originalName: ObjCMsgSendVariant.normal.name,
returnType: returnType,
parameters: _params(params),
isInternal: true,
);
switch (variant) {
case ObjCMsgSendVariant.normal:
variantFunc = null;
case ObjCMsgSendVariant.fpret:
variantFunc = Func(
name: '${name}_fpret',
originalName: variant.name,
returnType: returnType,
parameters: _params(params),
isInternal: true,
);
case ObjCMsgSendVariant.stret:
variantFunc = Func(
name: '${name}_stret',
originalName: variant.name,
returnType: voidType,
parameters: _params(params, structRetPtr: PointerType(returnType)),
isInternal: true,
);
}
}
static List<Parameter> _params(List<ObjCMethodParam> params,
{Type? structRetPtr}) {
return [
if (structRetPtr != null) Parameter(name: 'stret', type: structRetPtr),
Parameter(name: 'obj', type: PointerType(objCObjectType)),
Parameter(name: 'sel', type: PointerType(objCSelType)),
for (final p in params) Parameter(name: p.name, type: p.type),
];
}
bool get isStret => variant == ObjCMsgSendVariant.stret;
void addDependencies(Set<Binding> dependencies) {
if (variant != ObjCMsgSendVariant.normal) {
useVariants.addDependencies(dependencies);
}
normalFunc.addDependencies(dependencies);
variantFunc?.addDependencies(dependencies);
}
String invoke(String lib, String target, String sel, Iterable<String> params,
{String? structRetPtr}) {
final normalCall = _invoke(normalFunc.name, lib, target, sel, params);
switch (variant) {
case ObjCMsgSendVariant.normal:
return normalCall;
case ObjCMsgSendVariant.fpret:
final fpretCall = _invoke(variantFunc!.name, lib, target, sel, params);
return '$lib.${useVariants.name} ? $fpretCall : $normalCall';
case ObjCMsgSendVariant.stret:
final stretCall = _invoke(variantFunc!.name, lib, target, sel, params,
structRetPtr: structRetPtr);
return '$lib.${useVariants.name} ? $stretCall : '
'$structRetPtr.ref = $normalCall';
}
}
static String _invoke(
String name,
String lib,
String target,
String sel,
Iterable<String> params, {
String? structRetPtr,
}) {
return '''$lib.$name(${[
if (structRetPtr != null) structRetPtr,
target,
sel,
...params,
].join(', ')})''';
}
}