blob: 53697c3013c78dc12e05c69b3be2c40ac344e02d [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 '../code_generator.dart';
import '../context.dart';
import '../strings.dart' as strings;
import '../visitor/ast.dart';
import 'binding_string.dart';
import 'scope.dart';
import 'writer.dart';
class ObjCBlock extends BindingType with HasLocalScope {
final Context context;
final Type returnType;
final List<Parameter> params;
final bool returnsRetained;
ObjCBlockWrapperFuncs? _blockWrappers;
ObjCProtocolMethodTrampoline? protocolTrampoline;
final Parameter _blockParam;
final Parameter _waiterParam;
late final _FnHelper _helper;
late final _FnHelper _blockingHelper;
factory ObjCBlock(
Context context, {
required Type returnType,
required List<Parameter> params,
required bool returnsRetained,
}) {
final renamedParams = [
for (var i = 0; i < params.length; ++i)
Parameter(
name: 'arg$i',
type: params[i].type,
objCConsumed: params[i].objCConsumed,
),
];
final usr = _getBlockUsr(returnType, renamedParams, returnsRetained);
final oldBlock = context.bindingsIndex.getSeenObjCBlock(usr);
if (oldBlock != null) {
return oldBlock;
}
final block = ObjCBlock._(
context,
usr: usr,
name: _getBlockName(returnType, renamedParams.map((a) => a.type)),
returnType: returnType,
params: renamedParams,
returnsRetained: returnsRetained,
);
context.bindingsIndex.addObjCBlockToSeen(usr, block);
return block;
}
ObjCBlock._(
this.context, {
required String super.usr,
required super.name,
required this.returnType,
required this.params,
required this.returnsRetained,
}) : _waiterParam = Parameter(
name: 'waiter',
type: PointerType(voidType),
objCConsumed: false,
),
_blockParam = Parameter(
type: PointerType(objCBlockType),
name: 'block',
objCConsumed: false,
),
super(originalName: usr) {
_helper = _FnHelper(context, returnType, params, _blockParam);
_blockingHelper = _FnHelper(context, returnType, [
_waiterParam,
...params,
], _blockParam);
if (hasListener) {
_blockWrappers = context.objCBuiltInFunctions.getBlockTrampolines(this);
}
}
void fillProtocolTrampoline() {
protocolTrampoline ??= context.objCBuiltInFunctions
.getProtocolMethodTrampoline(this);
}
// Generates a human readable name for the block based on the args and return
// type. These names will be pretty verbose and unweildy, but they're at least
// sensible and stable. Users can always add their own typedef with a simpler
// name if necessary.
static String _getBlockName(Type returnType, Iterable<Type> argTypes) =>
'ObjCBlock_${[returnType, ...argTypes].map(_typeName).join('_')}';
static String _typeName(Type type) =>
type.toString().replaceAll(_illegalNameChar, '');
static final _illegalNameChar = RegExp(r'[^0-9a-zA-Z]');
static String _getBlockUsr(
Type returnType,
List<Parameter> params,
bool returnsRetained,
) {
// Create a fake USR code for the block. This code is used to dedupe blocks
// with the same signature. Not intended to be human readable.
return [
'${strings.synthUsrChar} objcBlock:',
'${returnType.cacheKey()} ${returnsRetained ? 'R' : ''}',
for (final param in params)
'${param.type.cacheKey()} ${param.objCConsumed ? 'C' : ''}',
].join(' ');
}
bool get hasListener => returnType == voidType;
String _blockType(Context context) {
final argStr = params
.map((param) {
final type = param.type.getObjCBlockSignatureType(context);
return param.objCConsumed
? '${ObjCBuiltInFunctions.consumedType.gen(context)}<$type>'
: type;
})
.join(', ');
final retType = returnType.getObjCBlockSignatureType(context);
final retStr = returnsRetained
? '${ObjCBuiltInFunctions.retainedType.gen(context)}<$retType>'
: retType;
final func = '$retStr Function($argStr)';
return '${ObjCBuiltInFunctions.blockType.gen(context)}<$func>';
}
@override
BindingString toBindingString(Writer w) {
final s = StringBuffer();
final context = w.context;
final voidPtr = PointerType(voidType);
final blockPtr = PointerType(objCBlockType);
final funcPtrTrampoline = localScope.addPrivate('_fnPtrTrampoline');
final closureTrampoline = localScope.addPrivate('_closureTrampoline');
final funcPtrCallable = localScope.addPrivate('_fnPtrCallable');
final closureCallable = localScope.addPrivate('_closureCallable');
final listenerTrampoline = localScope.addPrivate('_listenerTrampoline');
final listenerCallable = localScope.addPrivate('_listenerCallable');
final blockingTrampoline = localScope.addPrivate('_blockingTrampoline');
final blockingCallable = localScope.addPrivate('_blockingCallable');
final blockingListenerCallable = localScope.addPrivate(
'_blockingListenerCallable',
);
final newPointerBlock = ObjCBuiltInFunctions.newPointerBlock.gen(context);
final newClosureBlock = ObjCBuiltInFunctions.newClosureBlock.gen(context);
final getBlockClosure = ObjCBuiltInFunctions.getBlockClosure.gen(context);
final releaseFn = ObjCBuiltInFunctions.objectRelease.gen(context);
final objCContext = ObjCBuiltInFunctions.objCContext.gen(context);
final signalWaiterFn = ObjCBuiltInFunctions.signalWaiter.gen(context);
final returnFfiDartType = returnType.getFfiDartType(context);
final voidPtrCType = voidPtr.getCType(context);
final blockCType = blockPtr.getCType(context);
final blockType = _blockType(context);
final defaultValue = returnType.getDefaultValue(context);
final exceptionalReturn = defaultValue == null ? '' : ', $defaultValue';
final ffiPrefix = w.context.libs.prefix(ffiImport);
final paramsNameOnly = _helper.paramsNameOnly;
final trampNatCallType = _helper.trampNatCallType;
// Snippet that converts a Dart typed closure to FfiDart type. This snippet
// is used below. Note that the closure being converted is called `fn`.
final convertedFnArgs = params
.map(
(p) => p.type.convertFfiDartTypeToDartType(
context,
p.name,
objCRetain: !p.objCConsumed,
),
)
.join(', ');
final convFnInvocation = returnType.convertDartTypeToFfiDartType(
context,
'fn($convertedFnArgs)',
objCRetain: true,
objCAutorelease: !returnsRetained,
);
final convFn = '(${_helper.paramsFfiDartType}) => $convFnInvocation';
// Write the wrapper class.
s.write('''
/// Construction methods for `$blockType`.
abstract final class $name {
/// Returns a block that wraps the given raw block pointer.
static $blockType fromPointer($blockCType pointer,
{bool retain = false, bool release = false}) =>
$blockType(pointer, retain: retain, release: release);
/// Creates a block from a C function pointer.
///
/// This block must be invoked by native code running on the same thread as
/// the isolate that registered it. Invoking the block on the wrong thread
/// will result in a crash.
static $blockType fromFunctionPointer(${_helper.natFnPtrCType} ptr) =>
$blockType($newPointerBlock($funcPtrCallable, ptr.cast()),
retain: false, release: true);
/// Creates a block from a Dart function.
///
/// This block must be invoked by native code running on the same thread as
/// the isolate that registered it. Invoking the block on the wrong thread
/// will result in a crash.
///
/// If `keepIsolateAlive` is true, this block will keep this isolate alive
/// until it is garbage collected by both Dart and ObjC.
static $blockType fromFunction(${_helper.dartType} fn,
{bool keepIsolateAlive = true}) =>
$blockType($newClosureBlock($closureCallable, $convFn, keepIsolateAlive),
retain: false, release: true);
''');
// Listener block constructor is only available for void blocks.
if (hasListener) {
// This snippet is the same as the convFn above, except that the params
// don't need to be retained because they've already been retained by
// _blockWrappers.listenerWrapper.
final listenerConvertedFnArgs = params
.map(
(p) => p.type.convertFfiDartTypeToDartType(
context,
p.name,
objCRetain: false,
),
)
.join(', ');
final listenerConvFnInvocation = returnType.convertDartTypeToFfiDartType(
context,
'fn($listenerConvertedFnArgs)',
objCRetain: true,
objCAutorelease: !returnsRetained,
);
final listenerConvFn =
'(${_helper.paramsFfiDartType}) => $listenerConvFnInvocation';
final wrapListenerFn = _blockWrappers!.listenerWrapper.name;
final wrapBlockingFn = _blockWrappers!.blockingWrapper.name;
s.write('''
/// Creates a listener block from a Dart function.
///
/// This is based on FFI's NativeCallable.listener, and has the same
/// capabilities and limitations. This block can be invoked from any thread,
/// but only supports void functions, and is not run synchronously. See
/// NativeCallable.listener for more details.
///
/// If `keepIsolateAlive` is true, this block will keep this isolate alive
/// until it is garbage collected by both Dart and ObjC.
static $blockType listener(${_helper.dartType} fn,
{bool keepIsolateAlive = true}) {
final raw = $newClosureBlock($listenerCallable.nativeFunction.cast(),
$listenerConvFn, keepIsolateAlive);
final wrapper = $wrapListenerFn(raw);
$releaseFn(raw.cast());
return $blockType(wrapper, retain: false, release: true);
}
/// Creates a blocking block from a Dart function.
///
/// This callback can be invoked from any native thread, and will block the
/// caller until the callback is handled by the Dart isolate that created
/// the block. Async functions are not supported.
///
/// If `keepIsolateAlive` is true, this block will keep this isolate alive
/// until it is garbage collected by both Dart and ObjC. If the owner isolate
/// has shut down, and the block is invoked by native code, it may block
/// indefinitely, or have other undefined behavior.
static $blockType blocking(${_helper.dartType} fn,
{bool keepIsolateAlive = true}) {
final raw = $newClosureBlock($blockingCallable.nativeFunction.cast(),
$listenerConvFn, keepIsolateAlive);
final rawListener = $newClosureBlock(
$blockingListenerCallable.nativeFunction.cast(),
$listenerConvFn, keepIsolateAlive);
final wrapper = $wrapBlockingFn(raw, rawListener, $objCContext);
$releaseFn(raw.cast());
$releaseFn(rawListener.cast());
return $blockType(wrapper, retain: false, release: true);
}
static $returnFfiDartType $listenerTrampoline(
$blockCType block, ${_helper.paramsFfiDartType}) {
($getBlockClosure(block) as ${_helper.ffiDartType})($paramsNameOnly);
$releaseFn(block.cast());
}
static $trampNatCallType $listenerCallable =
$trampNatCallType.listener($listenerTrampoline $exceptionalReturn)
..keepIsolateAlive = false;
static $returnFfiDartType $blockingTrampoline(
$blockCType block, ${_blockingHelper.paramsFfiDartType}) {
try {
($getBlockClosure(block) as ${_helper.ffiDartType})($paramsNameOnly);
} catch (e) {
} finally {
$signalWaiterFn(waiter);
$releaseFn(block.cast());
}
}
static ${_blockingHelper.trampNatCallType} $blockingCallable =
${_blockingHelper.trampNatCallType}.isolateLocal(
$blockingTrampoline $exceptionalReturn)..keepIsolateAlive = false;
static ${_blockingHelper.trampNatCallType} $blockingListenerCallable =
${_blockingHelper.trampNatCallType}.listener(
$blockingTrampoline $exceptionalReturn)..keepIsolateAlive = false;
''');
}
s.write('''
static $returnFfiDartType $funcPtrTrampoline(
$blockCType block, ${_helper.paramsFfiDartType}) =>
block.ref.target.cast<${_helper.natFnFfiDartType}>()
.asFunction<${_helper.ffiDartType}>()($paramsNameOnly);
static $voidPtrCType $funcPtrCallable = $ffiPrefix.Pointer.fromFunction<
${_helper.trampCType}>($funcPtrTrampoline $exceptionalReturn).cast();
static $returnFfiDartType $closureTrampoline(
$blockCType block, ${_helper.paramsFfiDartType}) =>
($getBlockClosure(block) as ${_helper.ffiDartType})($paramsNameOnly);
static $voidPtrCType $closureCallable = $ffiPrefix.Pointer.fromFunction<
${_helper.trampCType}>($closureTrampoline $exceptionalReturn).cast();
}
''');
// Call operator extension method.
s.write('''
/// Call operator for `$blockType`.
extension $name\$CallExtension on $blockType {
${returnType.getDartType(context)} call(${_helper.paramsDartType}) =>''');
final callMethodArgs = params
.map(
(p) => p.type.convertDartTypeToFfiDartType(
context,
p.name,
objCRetain: p.objCConsumed,
objCAutorelease: false,
),
)
.join(', ');
final callMethodInvocation =
'''
ref.pointer.ref.invoke.cast<${_helper.trampNatFnCType}>()
.asFunction<${_helper.trampFfiDartType}>()(
ref.pointer, $callMethodArgs)''';
s.write(
returnType.convertFfiDartTypeToDartType(
context,
callMethodInvocation,
objCRetain: !returnsRetained,
),
);
s.write(';\n');
s.write('}\n\n');
return BindingString(
type: BindingStringType.objcBlock,
string: s.toString(),
);
}
@override
BindingString? toObjCBindingString(Writer w) {
final chunks = [
_blockWrappersBindingString(w),
_protocolTrampolineBindingString(w),
].nonNulls;
if (chunks.isEmpty) return null;
return BindingString(
type: BindingStringType.objcBlock,
string: chunks.join(''),
);
}
String? _blockWrappersBindingString(Writer w) {
if (_blockWrappers?.objCBindingsGenerated ?? true) return null;
_blockWrappers!.objCBindingsGenerated = true;
final argsReceived = <String>[];
final retains = <String>[];
for (var i = 0; i < params.length; ++i) {
final param = params[i];
final argName = 'arg$i';
argsReceived.add(param.getNativeType(varName: argName));
retains.add(param.type.generateRetain(argName) ?? argName);
}
final blockingRetains = ['nil', ...retains];
final blockingListenerRetains = [_waiterParam.name, ...retains];
final argStr = argsReceived.join(', ');
final declArgStr = argStr.isEmpty ? 'void' : argStr;
final blockingArgStr = [
_waiterParam.getNativeType(varName: _waiterParam.name),
...argsReceived,
].join(', ');
final listenerWrapper = _blockWrappers!.listenerWrapper.name;
final blockingWrapper = _blockWrappers!.blockingWrapper.name;
final listenerName = Namer.cSafeName(
context.rootObjCScope.addPrivate('_ListenerTrampoline'),
);
final blockingName = Namer.cSafeName(
context.rootObjCScope.addPrivate('_BlockingTrampoline'),
);
return '''
typedef ${returnType.getNativeType()} (^$listenerName)($declArgStr);
__attribute__((visibility("default"))) __attribute__((used))
$listenerName $listenerWrapper($listenerName block) NS_RETURNS_RETAINED {
return ^void($argStr) {
${generateRetain('block')};
block(${retains.join(', ')});
};
}
typedef ${returnType.getNativeType()} (^$blockingName)($blockingArgStr);
__attribute__((visibility("default"))) __attribute__((used))
$listenerName $blockingWrapper(
$blockingName block, $blockingName listenerBlock,
DOBJC_Context* ctx) NS_RETURNS_RETAINED {
BLOCKING_BLOCK_IMPL(ctx, ^void($argStr), {
${generateRetain('block')};
block(${blockingRetains.join(', ')});
}, {
${generateRetain('listenerBlock')};
listenerBlock(${blockingListenerRetains.join(', ')});
});
}
''';
}
String? _protocolTrampolineBindingString(Writer w) {
if (protocolTrampoline?.objCBindingsGenerated ?? true) return null;
protocolTrampoline!.objCBindingsGenerated = true;
final argsReceived = <String>[];
final argsPassed = <String>[];
for (var i = 0; i < params.length; ++i) {
final param = params[i];
final argName = i == 0 ? 'sel' : 'arg$i';
argsReceived.add(param.getNativeType(varName: argName));
argsPassed.add(argName);
}
final ret = returnType.getNativeType();
final argRecv = argsReceived.join(', ');
final argPass = argsPassed.join(', ');
final fnName = protocolTrampoline!.func.name;
final block = Namer.cSafeName(
context.rootObjCScope.addPrivate('_ProtocolTrampoline'),
);
final msgSend = '((id (*)(id, SEL, SEL))objc_msgSend)';
final getterSel = '@selector(getDOBJCDartProtocolMethodForSelector:)';
final blkGetter = '(($block)$msgSend(target, $getterSel, sel))';
return '''
typedef $ret (^$block)($argRecv);
__attribute__((visibility("default"))) __attribute__((used))
$ret $fnName(id target, $argRecv) {
return $blkGetter($argPass);
}
''';
}
@override
String getCType(Context context) =>
PointerType(objCBlockType).getCType(context);
// We return `ObjCBlockBase<T>` here instead of the code genned wrapper, so
// that the subtyping rules work as expected.
// See https://github.com/dart-lang/native/issues/1416 for details.
@override
String getDartType(Context context) => _blockType(context);
@override
String getObjCBlockSignatureType(Context context) => getDartType(context);
@override
String getNativeType({String varName = ''}) => 'id $varName';
@override
bool get sameFfiDartAndCType => true;
@override
bool get sameDartAndCType => false;
@override
bool get sameDartAndFfiDartType => false;
@override
String convertDartTypeToFfiDartType(
Context context,
String value, {
required bool objCRetain,
required bool objCAutorelease,
}) => ObjCInterface.generateGetId(value, objCRetain, objCAutorelease);
@override
String convertFfiDartTypeToDartType(
Context context,
String value, {
required bool objCRetain,
String? objCEnclosingClass,
}) => ObjCInterface.generateConstructor(name, value, objCRetain);
@override
String? generateRetain(String value) => 'objc_retainBlock($value)';
@override
String toString() =>
'($returnType (^)(${params.map((p) => p.type.toString()).join(', ')}))';
@override
void visitChildren(Visitor visitor) {
super.visitChildren(visitor);
visitor.visit(returnType);
visitor.visitAll(params);
visitor.visit(_blockWrappers);
visitor.visit(protocolTrampoline);
visitor.visit(ffiImport);
visitor.visit(objcPkgImport);
_helper.visitChildren(visitor);
_blockingHelper.visitChildren(visitor);
}
@override
bool isSupertypeOf(Type other) {
other = other.typealiasType;
if (other is ObjCBlock) {
return Type.isSupertypeOfVariance(
covariantLeft: [returnType],
covariantRight: [other.returnType],
contravariantLeft: params.map((p) => p.type).toList(),
contravariantRight: other.params.map((p) => p.type).toList(),
);
}
return false;
}
}
class _FnHelper {
final Context context;
final FunctionType fnType;
final FunctionType trampFnType;
late final NativeFunc natFnType;
late final String natFnFfiDartType = natFnType.getFfiDartType(context);
late final String natFnPtrCType = PointerType(natFnType).getCType(context);
late final String dartType = fnType.getDartType(
context,
writeArgumentNames: false,
);
late final String ffiDartType = fnType.getFfiDartType(
context,
writeArgumentNames: false,
);
late final String trampCType = trampFnType.getCType(
context,
writeArgumentNames: false,
);
late final String trampFfiDartType = trampFnType.getFfiDartType(
context,
writeArgumentNames: false,
);
late final String trampNatCallType =
'${context.libs.prefix(ffiImport)}.NativeCallable<$trampCType>';
late final String trampNatFnCType = NativeFunc(trampFnType).getCType(context);
late final String paramsNameOnly = fnType.parameters
.map((p) => p.name)
.join(', ');
late final String paramsFfiDartType = fnType.parameters
.map((p) => '${p.type.getFfiDartType(context)} ${p.name}')
.join(', ');
late final String paramsDartType = fnType.parameters
.map((p) => '${p.type.getDartType(context)} ${p.name}')
.join(', ');
_FnHelper(
this.context,
Type returnType,
List<Parameter> params,
Parameter blockParam,
) : fnType = FunctionType(returnType: returnType, parameters: params),
trampFnType = FunctionType(
returnType: returnType,
parameters: [blockParam, ...params],
) {
natFnType = NativeFunc(fnType);
}
void visitChildren(Visitor visitor) {
visitor.visit(fnType);
visitor.visit(trampFnType);
visitor.visit(natFnType);
}
}