Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions pkgs/ffigen/doc/objc_api_versioning.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two overall comments:

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Dealing with Objective-C API versioning

Objective-C uses the `@available` annotation to allow developers to write if
statements that do different things on different OS versions. It also generates
a compiler warning if the developer uses an API that is only available in
particular OS versions, without guarding it with an `@available` if statement.

```obj-c
if (@available(iOS 18, *)) {
// Use newer iOS 18 API.
} else {
// Fallback to old API.
}
```

We can't replicate the compiler warning in Dart/FFIgen at the moment, but we
can write the runtime version check. The recommended way of doing this is using
`package:objective_c`'s `checkOsVersion`:

```dart
// If you only need to support iOS:
if (checkOsVersion(iOS: Version(18, 0, 0))) {
// Use newer iOS 18 API.
} else {
// Fallback to old API.
}

// If you need to support iOS and macOS:
if (checkOsVersion(iOS: Version(18, 0, 0), macOS: Version(15, 3, 0))) {
// Use newer API available in iOS 18 and macOS 15.3.
} else {
// Fallback to old API.
}
```

`checkOsVersion` returns `true` if the current OS version is equal to or greater
than the given version.

FFIgen's generated code includes version checks that will throw an
`OsVersionError` if the API is not available in the current OS version. But it's
better to write if statements like above, rather than trying to catch this
error.
50 changes: 50 additions & 0 deletions pkgs/ffigen/doc/objc_gc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Objective-C memory management considerations

Objective-C uses reference counting to delete objects that are no longer in
use, and Dart uses garbage collection (GC) for this. The Dart wrapper objects
that wrap Objective-C objects automatically increment the reference count
when the wrapper is created, and decrement it when the wrapper is destroyed.
For the most part this is all automatic and you don't need to worry about it.

However, when using blocks or protocols, it's possible to create reference
cycles that will prevent these objects from being cleaned up. If a
block/protocol method closes over a wrapper object that holds a reference
to the block/protocol, this cycle will cause a memory leak. This example
uses a protocol, but the same thing can happen with a block:

```dart
final foo = FooInterface();
foo.delegate = BarDelegate.implement(
someMethod: () {
foo.anotherMethod();
}
);
```

`foo.delegate` is holding a reference to a `BarDelegate` whose
`someMethod` implementation is a Dart function which implicitly
holds a reference to the `foo`. If this was all pure Dart code, the
garbage collector would be able to clean up this reference cycle, but
since `FooInterface` and `BarDelegate` are both Objective-C types this will
leak memory.

![Objective-C/Dart reference cycle](objc_ref_cycle.svg "Objective-C/Dart reference cycle")

To break this cycle, the method implementation must hold `foo` as a
`WeakReference`, but it's even better to write your methods to avoid the
need for things like this. To ensure the method doesn't capture anything
unexpected, it's also a good idea to move its construction to a separate
function.

```dart
BarDelegate createBarDelegate(WeakReference<FooInterface> weakFoo) {
return BarDelegate.implement(
someMethod: () {
weakFoo.target?.anotherMethod();
}
);
}

final foo = FooInterface();
foo.delegate = createBarDelegate(WeakReference(foo));
```
102 changes: 102 additions & 0 deletions pkgs/ffigen/doc/objc_ios_mac_differences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Dealing with iOS/macOS API differences

Objective-C can guard iOS/macOS specific code with `#if` macros, so that it
will only be compiled on specific platforms:

```obj-c
#if TARGET_OS_IPHONE
@interface WKWebView : UIView
#else
@interface WKWebView : NSView
#endif
```

Dart has no equivalent of this, because Dart's kernel files are designed to
work on any platform. `Platform.isMacOS` etc are runtime values, not compile
time constants (technical detail: they're compile time constant in AOT mode,
but not in JIT mode). So it's not possible to conditionally import dart files
based on `Platform.isMacOS`/`isIOS`.

There are two approaches for dealing with platform differences, depending
on how significant the differences are. If the API you're working with only
has small differences between iOS and macOS, you can try to generate a
single FFIgen wrapper for both, and use runtime `Platform.isMacOS`/`isIOS`
checks to call different methods. At runtime, FFIgen lazy loads all classes
and methods, so if a class/method doesn't exist in your plugin/app's native
code, that's fine as long as the class/method isn't used at runtime.

```dart
final foo = Foo();
if (Platform.isMacOS) {
final bar = MacSpecificBar();
foo.macSpecificMethod(bar);
} else {
assert(Platform.isIOS);
final bar = IosSpecificBar();
foo.iosSpecificMethod(bar);
}
```

If the API differences are severe enough that they would cause compile time
errors if they're in a single library, that approach won't work. For example,
`WKWebView` inherits from `UIView` on iOS and `NSView` on macOS, so there's
no way FFIgen can generate a single `WKWebView` class for both platforms.
In cases like this, it's necessary to run FFIgen separately for each
platform, and generate different bindings for iOS and macOS. Since you can't
conditionally import based on the OS, you need a way of pulling both sets
of bindings into your plugin. The simplest approach is to use the rename
config option to rename the APIs so they don't conflict (eg `WKWebViewIOS`
and `WKWebViewMacOS`), then import both sets of bindings and use `Platform`
checks to call different APIs.

```dart
if (Platform.isMacOS) {
final webView = WKWebViewMacOS();
// ...
} else {
assert(Platform.isIOS);
final webView = WKWebViewIOS();
// ...
}
```

If renaming isn't practical (eg there would be too many renames or `Platform`
checks), then you can write separate Dart classes for each platform that
implement a shared interface:

```dart
// Both imports were generated by FFIgen.
import 'web_view_bindings_mac.dart' as mac;
import 'web_view_bindings_ios.dart' as ios;

abstract interface class WebView {
void load(Uri uri);

factory WebView() {
if (Platform.isMacOS) {
return WebViewMac();
} else {
assert(Platform.isIOS);
return WebViewIOS();
}
}
}

class WebViewMac implements WebView {
mac.WKWebView _view;

@override
void load(Uri uri) {
// ...
}
}

class WebViewIOS implements WebView {
ios.WKWebView _view;

@override
void load(Uri uri) {
// ...
}
}
```
1 change: 1 addition & 0 deletions pkgs/ffigen/doc/objc_ref_cycle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions pkgs/ffigen/doc/objc_runtime_types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Runtime type checks in Objective-C

Ordinary Dart has a distinction between an object's static type and its
runtime type:

```dart
class Base {}
class Child extends Base {}

Base x = Child(); // x has a static type of Base
print(x.runtimeType); // but a runtime type of Child
```

The static type determines at compile time what methods are
allowed to be invoked on an object, and the runtime type determines
which class's method implementation is actually invoked at runtime.

When doing Objective-C interop (or Java interop for that matter),
another layer of typing is added to each variable. As well as the
Dart static type and runtime type, there is also the Objective-C
runtime type to consider. The (Dart static/Dart runtime/Objective-C runtime)
types could come in just about any combination eg (`Base`/`Child`/`Child`),
(`Base`/`Base`/`Child`), or even (`Base`/`Child`/`Grandchild`).

Just like in the pure Dart case, the Dart static type determines
what methods are allowed to be invoked, but now the method
implementation that is actually invoked at run time is determined
by the *Objective-C* runtime type. In fact, the Dart runtime type is
completely irrelevant when doing Objective-C interop.

Dart's `is` keyword checks the Dart runtime type. You shouldn't use
this on Objective-C objects, because the Dart runtime type is irrelevant, and
often won't match the Objective-C runtime type. Instead of `x is Foo`,
use `Foo.isInstance(x)`.

Dart's `as` keyword changes the static type of an object (and also
checks its runtime type). You shouldn't use this on Objective-C objects,
because the runtime type check may fail since the Dart runtime
type often won't match the Objective-C runtime type. Instead of `x as Foo`,
use `Foo.castFrom(x)`.
47 changes: 47 additions & 0 deletions pkgs/ffigen/doc/objc_threading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Objective-C threading considerations

Multithreading is one of the trickiest parts of interop between Objective-C
and Dart. This is due to the relationship between Dart isolates and OS
threads, and the way Apple's APIs handle multithreading:

1. Dart isolates are not the same thing as threads. Isolates run on threads,
but aren't guaranteed to run on any particular thread, and the VM might
change which thread an isolate is running on without warning. There is an
[open feature request](https://github.com/dart-lang/sdk/issues/46943)
to enable isolates to be pinned to specific threads.
2. While FFIgen supports converting Dart functions to Objective-C blocks,
most Apple APIs don't make any guarantees about which thread a callback
will run on.
3. Most APIs that involve UI interaction can only be called on the main
thread, also called the platform thread in Flutter.
4. Many Apple APIs are [not thread safe](
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html).

The first two points mean that a block created in one isolate might be
invoked on a thread running a different isolate, or no isolate at all.
Depending on the type of block you are using, this could cause your app to
crash. When a block is created, the isolate it was created in is its owner.
Blocks created using `FooBlock.fromFunction` must be invoked on the
owner isolate's thread, otherwise they will crash. Blocks created using
`FooBlock.listener` or `FooBlock.blocking` can be safely invoked from any
thread, and the function they wrap will (eventually) be invoked inside the
owner isolate, though these constructors are only supported for blocks that
return `void`. `FooBlock.blocking` may add support for non-`void` return values
in future, if there is user demand for it.

The third point means that directly calling some Apple APIs using the
generated Dart bindings might be thread unsafe. This could crash your app, or
cause other unpredictable behavior. In recent versions of Flutter, the main
isolate runs on the platform thread, so this isn't an issue when invoking
these thread-locked APIs from the main isolate. If you need to invoke these
APIs from other isolates, or you need to support older versions of flutter,
you can use the [`runOnPlatformThread`](
https://api.flutter.dev/flutter/dart-ui/runOnPlatformThread.html) function.

Regarding the last point, although Dart isolates can switch threads, they
only ever run on one thread at a time. So, the API you are interacting with
doesn't necessarily have to be thread safe, as long as it is not thread
hostile, and doesn't have constraints about which thread it's called from.

You can safely interact with Objective-C code as long as you keep these
limitations in mind.
Loading