blob: 5f4b4c0c4b28114dfdeee5dc734cb9d2d71ea7d6 [file] [log] [blame] [edit]
// 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.
import 'dart:ffi';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'c_bindings_generated.dart' as c;
typedef ObjectPtr = Pointer<c.ObjCObject>;
typedef BlockPtr = Pointer<c.ObjCBlockImpl>;
typedef VoidPtr = Pointer<Void>;
final class UseAfterReleaseError extends StateError {
UseAfterReleaseError() : super('Use after release error');
}
final class DoubleReleaseError extends StateError {
DoubleReleaseError() : super('Double release error');
}
final class UnimplementedOptionalMethodException implements Exception {
final String clazz;
final String method;
UnimplementedOptionalMethodException(this.clazz, this.method);
@override
String toString() =>
'$runtimeType: Instance of $clazz does not implement $method';
}
final class FailedToLoadClassException implements Exception {
final String clazz;
FailedToLoadClassException(this.clazz);
@override
String toString() => '$runtimeType: Failed to load Objective-C class: $clazz';
}
final class FailedToLoadProtocolException implements Exception {
final String protocol;
FailedToLoadProtocolException(this.protocol);
@override
String toString() =>
'$runtimeType: Failed to load Objective-C protocol: $protocol';
}
/// Failed to load a method of a protocol.
///
/// This means that a method that was seen in the protocol declaration at
/// compile time was missing from the protocol at runtime. This is usually
/// caused by a version mismatch between the compile time header and the runtime
/// framework (eg, running an app on an older iOS device).
///
/// To fix this, check whether the method exists at runtime, using
/// `ObjCProtocolMethod.isAvailable`, and implement fallback logic if it's
/// missing.
final class FailedToLoadProtocolMethodException implements Exception {
final String protocol;
final String method;
FailedToLoadProtocolMethodException(this.protocol, this.method);
@override
String toString() =>
'$runtimeType: Failed to load Objective-C protocol method: '
'$protocol.$method';
}
final class ObjCRuntimeError extends Error {
final String message;
ObjCRuntimeError(this.message);
@override
String toString() => '$runtimeType: $message';
}
extension GetProtocolName on Pointer<c.ObjCProtocol> {
/// Returns the name of the protocol.
String get name => c.getProtocolName(this).cast<Utf8>().toDartString();
}
/// Only for use by ffigen bindings.
Pointer<c.ObjCSelector> registerName(String name) {
final cstr = name.toNativeUtf8();
final sel = c.registerName(cstr.cast());
calloc.free(cstr);
return sel;
}
/// Only for use by ffigen bindings.
ObjectPtr getClass(String name) {
final cstr = name.toNativeUtf8();
final clazz = c.getClass(cstr.cast());
calloc.free(cstr);
if (clazz == nullptr) {
throw FailedToLoadClassException(name);
}
return clazz;
}
/// Only for use by ffigen bindings.
Pointer<c.ObjCProtocol> getProtocol(String name) {
final cstr = name.toNativeUtf8();
final clazz = c.getProtocol(cstr.cast());
calloc.free(cstr);
if (clazz == nullptr) {
throw FailedToLoadProtocolException(name);
}
return clazz;
}
/// Only for use by ffigen bindings.
Pointer<Char>? getProtocolMethodSignature(
Pointer<c.ObjCProtocol> protocol,
Pointer<c.ObjCSelector> sel, {
required bool isRequired,
required bool isInstanceMethod,
}) {
final sig =
c.getMethodDescription(protocol, sel, isRequired, isInstanceMethod).types;
return sig == nullptr ? null : sig;
}
/// Only for use by ffigen bindings.
final msgSendPointer =
Native.addressOf<NativeFunction<Void Function()>>(c.msgSend);
/// Only for use by ffigen bindings.
final msgSendFpretPointer =
Native.addressOf<NativeFunction<Void Function()>>(c.msgSendFpret);
/// Only for use by ffigen bindings.
final msgSendStretPointer =
Native.addressOf<NativeFunction<Void Function()>>(c.msgSendStret);
/// Only for use by ffigen bindings.
final useMsgSendVariants =
Abi.current() == Abi.iosX64 || Abi.current() == Abi.macosX64;
/// Only for use by ffigen bindings.
bool respondsToSelector(ObjectPtr obj, Pointer<c.ObjCSelector> sel) =>
_objcMsgSendRespondsToSelector(obj, _selRespondsToSelector, sel);
final _selRespondsToSelector = registerName('respondsToSelector:');
final _objcMsgSendRespondsToSelector = msgSendPointer
.cast<
NativeFunction<
Bool Function(ObjectPtr, Pointer<c.ObjCSelector>,
Pointer<c.ObjCSelector> aSelector)>>()
.asFunction<
bool Function(
ObjectPtr, Pointer<c.ObjCSelector>, Pointer<c.ObjCSelector>)>();
// _FinalizablePointer exists because we can't access `this` in the initializers
// of _ObjCReference's constructor, and we have to have an owner to attach the
// Dart_FinalizableHandle to. Ideally _ObjCReference would be the owner.
@pragma('vm:deeply-immutable')
final class _FinalizablePointer<T extends NativeType> implements Finalizable {
final Pointer<T> ptr;
_FinalizablePointer(this.ptr);
}
bool _dartAPIInitialized = false;
void _ensureDartAPI() {
if (!_dartAPIInitialized) {
final result = c.initializeApi(NativeApi.initializeApiDLData);
assert(result == 0);
_dartAPIInitialized = true;
}
}
c.Dart_FinalizableHandle _newFinalizableHandle(
_FinalizablePointer finalizable) {
_ensureDartAPI();
return c.newFinalizableHandle(finalizable, finalizable.ptr.cast());
}
Pointer<Bool> _newFinalizableBool(Object owner) {
_ensureDartAPI();
return c.newFinalizableBool(owner);
}
@pragma('vm:deeply-immutable')
abstract final class _ObjCReference<T extends NativeType>
implements Finalizable {
final _FinalizablePointer<T> _finalizable;
final c.Dart_FinalizableHandle? _ptrFinalizableHandle;
final Pointer<Bool> _isReleased;
_ObjCReference(this._finalizable,
{required bool retain, required bool release})
: _ptrFinalizableHandle =
release ? _newFinalizableHandle(_finalizable) : null,
_isReleased = _newFinalizableBool(_finalizable) {
assert(_isValid(_finalizable.ptr));
if (retain) {
_retain(_finalizable.ptr);
}
}
bool get isReleased => _isReleased.value;
void _release(void Function(ObjectPtr) releaser) {
if (isReleased) {
throw DoubleReleaseError();
}
assert(_isValid(_finalizable.ptr));
if (_ptrFinalizableHandle != null) {
c.deleteFinalizableHandle(_ptrFinalizableHandle, _finalizable);
releaser(_finalizable.ptr.cast());
}
_isReleased.value = true;
}
void release() => _release(c.objectRelease);
Pointer<T> autorelease() {
_release(c.objectAutorelease);
return _finalizable.ptr;
}
@override
bool operator ==(Object other) =>
other is _ObjCReference && _finalizable.ptr == other._finalizable.ptr;
@override
int get hashCode => _finalizable.ptr.hashCode;
Pointer<T> get pointer {
if (isReleased) {
throw UseAfterReleaseError();
}
assert(_isValid(_finalizable.ptr));
return _finalizable.ptr;
}
Pointer<T> retainAndReturnPointer() {
final ptr = pointer;
_retain(ptr);
return ptr;
}
Pointer<T> retainAndAutorelease() {
final ptr = pointer;
_retain(ptr);
c.objectAutorelease(ptr.cast());
return ptr;
}
void _retain(Pointer<T> ptr);
bool _isValid(Pointer<T> ptr);
}
// Wrapper around ObjCObjectRef/ObjCBlockRef. This is needed because
// deeply-immutable classes must be final, but the ffigen bindings need to
// extend ObjCObjectBase/ObjCBlockBase.
class _ObjCRefHolder<T extends NativeType, Ref extends _ObjCReference<T>> {
final Ref ref;
_ObjCRefHolder(this.ref);
@override
bool operator ==(Object other) => other is _ObjCRefHolder && ref == other.ref;
@override
int get hashCode => ref.hashCode;
}
@pragma('vm:deeply-immutable')
final class ObjCObjectRef extends _ObjCReference<c.ObjCObject> {
ObjCObjectRef(ObjectPtr ptr, {required super.retain, required super.release})
: super(_FinalizablePointer(ptr));
@override
void _retain(ObjectPtr ptr) => c.objectRetain(ptr);
@override
bool _isValid(ObjectPtr ptr) => _isValidObject(ptr);
}
/// Only for use by ffigen bindings.
class ObjCObjectBase extends _ObjCRefHolder<c.ObjCObject, ObjCObjectRef> {
ObjCObjectBase(ObjectPtr ptr, {required bool retain, required bool release})
: super(ObjCObjectRef(ptr, retain: retain, release: release));
}
// Returns whether the object is valid and live. The pointer must point to
// readable memory, or be null. May (rarely) return false positives.
bool _isValidObject(ObjectPtr ptr) {
if (ptr == nullptr) return false;
return _isValidClass(c.getObjectClass(ptr));
}
final _allClasses = <ObjectPtr>{};
bool _isValidClass(ObjectPtr clazz, {bool forceReloadClasses = false}) {
if (!forceReloadClasses && _allClasses.contains(clazz)) return true;
// If the class is missing from the list, it either means we haven't created
// the set yet, or more classes have been loaded since we created the set, or
// the class is actually invalid. To rule out the first two cases, rebulid the
// set then try again. This is expensive, but only happens if asserts are
// enabled, and only happens more than O(1) times if there are actually
// invalid objects in use, which shouldn't happen in correct code.
final countPtr = calloc<UnsignedInt>();
final classList = c.copyClassList(countPtr);
final count = countPtr.value;
calloc.free(countPtr);
_allClasses.clear();
for (var i = 0; i < count; ++i) {
_allClasses.add(classList[i]);
}
calloc.free(classList);
return _allClasses.contains(clazz);
}
/// Only for use by ffigen bindings.
class ObjCProtocolBase extends ObjCObjectBase {
ObjCProtocolBase(super.ptr, {required super.retain, required super.release});
}
@pragma('vm:deeply-immutable')
final class ObjCBlockRef extends _ObjCReference<c.ObjCBlockImpl> {
ObjCBlockRef(BlockPtr ptr, {required super.retain, required super.release})
: super(_FinalizablePointer(ptr));
@override
void _retain(BlockPtr ptr) => c.blockRetain(ptr.cast());
@override
bool _isValid(BlockPtr ptr) => c.isValidBlock(ptr);
}
/// Only for use by ffigen bindings.
class ObjCBlockBase extends _ObjCRefHolder<c.ObjCBlockImpl, ObjCBlockRef> {
ObjCBlockBase(BlockPtr ptr, {required bool retain, required bool release})
: super(ObjCBlockRef(ptr, retain: retain, release: release));
}
Pointer<c.ObjCBlockDesc> _newBlockDesc(
Pointer<NativeFunction<Void Function(BlockPtr)>> disposeHelper) {
final desc = calloc.allocate<c.ObjCBlockDesc>(sizeOf<c.ObjCBlockDesc>());
desc.ref.reserved = 0;
desc.ref.size = sizeOf<c.ObjCBlockImpl>();
desc.ref.copy_helper = nullptr;
desc.ref.dispose_helper = disposeHelper.cast();
desc.ref.signature = nullptr;
return desc;
}
final _pointerBlockDesc = _newBlockDesc(nullptr);
final _closureBlockDesc = _newBlockDesc(
Native.addressOf<NativeFunction<Void Function(BlockPtr)>>(
c.disposeObjCBlockWithClosure));
BlockPtr _newBlock(VoidPtr invoke, VoidPtr target,
Pointer<c.ObjCBlockDesc> descriptor, int disposePort, int flags) {
final b = calloc.allocate<c.ObjCBlockImpl>(sizeOf<c.ObjCBlockImpl>());
b.ref.isa = Native.addressOf<Array<VoidPtr>>(c.NSConcreteGlobalBlock).cast();
b.ref.flags = flags;
b.ref.reserved = 0;
b.ref.invoke = invoke;
b.ref.target = target;
b.ref.dispose_port = disposePort;
b.ref.descriptor = descriptor;
assert(c.isValidBlock(b));
final copy = c.blockRetain(b.cast()).cast<c.ObjCBlockImpl>();
calloc.free(b);
assert(copy.ref.isa ==
Native.addressOf<Array<VoidPtr>>(c.NSConcreteMallocBlock).cast());
assert(c.isValidBlock(copy));
return copy;
}
const int _blockHasCopyDispose = 1 << 25;
/// Only for use by ffigen bindings.
BlockPtr newClosureBlock(VoidPtr invoke, Function fn, bool keepIsolateAlive) =>
_newBlock(
invoke,
_registerBlockClosure(fn, keepIsolateAlive),
_closureBlockDesc,
_blockClosureDisposer.sendPort.nativePort,
_blockHasCopyDispose);
/// Only for use by ffigen bindings.
BlockPtr newPointerBlock(VoidPtr invoke, VoidPtr target) =>
_newBlock(invoke, target, _pointerBlockDesc, 0, 0);
typedef _RegEntry = ({
Function closure,
RawReceivePort? keepAlivePort,
});
final _blockClosureRegistry = <int, _RegEntry>{};
int _blockClosureRegistryLastId = 0;
final _blockClosureDisposer = () {
_ensureDartAPI();
return RawReceivePort((dynamic msg) {
final id = msg as int;
assert(_blockClosureRegistry.containsKey(id));
final entry = _blockClosureRegistry.remove(id)!;
entry.keepAlivePort?.close();
}, 'ObjCBlockClosureDisposer')
..keepIsolateAlive = false;
}();
VoidPtr _registerBlockClosure(Function closure, bool keepIsolateAlive) {
++_blockClosureRegistryLastId;
assert(!_blockClosureRegistry.containsKey(_blockClosureRegistryLastId));
_blockClosureRegistry[_blockClosureRegistryLastId] = (
closure: closure,
keepAlivePort: keepIsolateAlive ? RawReceivePort() : null,
);
return VoidPtr.fromAddress(_blockClosureRegistryLastId);
}
/// Only for use by ffigen bindings.
Function getBlockClosure(BlockPtr block) {
var id = block.ref.target.address;
assert(_blockClosureRegistry.containsKey(id));
return _blockClosureRegistry[id]!.closure;
}
/// Only for use by ffigen bindings.
final Pointer<c.DOBJC_Context> objCContext =
c.fillContext(calloc<c.DOBJC_Context>());
// Not exported by ../objective_c.dart, because they're only for testing.
bool blockHasRegisteredClosure(BlockPtr block) =>
_blockClosureRegistry.containsKey(block.ref.target.address);
bool isValidBlock(BlockPtr block) => c.isValidBlock(block);
bool isValidClass(ObjectPtr clazz, {bool forceReloadClasses = false}) =>
_isValidClass(clazz, forceReloadClasses: forceReloadClasses);
bool isValidObject(ObjectPtr object) => _isValidObject(object);