-
Notifications
You must be signed in to change notification settings - Fork 111
[ffigen] Add more Objective-C documentation #2707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
c8f2186
[ffigen] Add more Objective-C documentation
liamappelbe d998573
update image
liamappelbe d9c3390
doc/README.md
liamappelbe 0f490bc
Merge branch 'main' into ffigen_docs
liamappelbe bd25b81
Update pkgs/ffigen/doc/objc_api_versioning.md
liamappelbe 8d9498a
Update pkgs/ffigen/doc/objc_api_versioning.md
liamappelbe 93cc8e6
Update pkgs/ffigen/doc/objc_api_versioning.md
liamappelbe 4d0871d
Update pkgs/ffigen/doc/objc_ios_mac_differences.md
liamappelbe bd7bfe1
Update pkgs/ffigen/doc/objc_runtime_types.md
liamappelbe a69abba
Merge branch 'ffigen_docs' of github.com:dart-lang/native into ffigen…
liamappelbe 8e26588
move docs around
liamappelbe d03e9d7
Update documentation for ObjC method filtering
liamappelbe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
liamappelbe marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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. | ||
liamappelbe marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```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 | ||
liamappelbe marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| error. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
|  | ||
|
|
||
| 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)); | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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` | ||
liamappelbe marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) { | ||
| // ... | ||
| } | ||
| } | ||
| ``` | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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`), | ||
liamappelbe marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| (`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)`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two overall comments:
docs/README.mdto this package that includes an index of all available docs in the do directory, similar to what Daco did in https://github.com/dart-lang/native/pull/2705/files#diff-a0842ea73f3a116eff7958fbf833dd2280c366c0d8985aa05f8e1403e0f39273