-
Notifications
You must be signed in to change notification settings - Fork 6k
Background Execution Implementation for iOS #5539
Conversation
@implementation FlutterHeadlessDartRunner { | ||
shell::ThreadHost _threadHost; | ||
std::unique_ptr<shell::Shell> _shell; | ||
} | ||
|
||
- (void)runWithEntrypoint:(NSString*)entrypoint { | ||
- (instancetype)init { |
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.
Does this function and dealloc below need to be added explicitly?
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.
No we don't. There was some other functionality added there before, but I forgot to remove these during cleanup. Will remove.
lib/ui/dart_runtime_hooks.cc
Outdated
library = Dart_LookupLibrary(library_name); | ||
DART_CHECK_VALID(library); | ||
} | ||
Dart_Handle closure = Dart_GetClosure(Dart_RootLibrary(), closure_name); |
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.
Dart_RootLibrary() -> library? Otherwise library is unused.
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.
Yes, will fix.
lib/ui/dart_runtime_hooks.cc
Outdated
} | ||
Dart_Handle owner = closure; | ||
Dart_Handle url = Dart_Null(); | ||
// TOOD(bkonyi): do we need to do this loop? |
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.
What happens when the closure is a static of a class instead of a top-level?
lib/ui/dart_runtime_hooks.cc
Outdated
@@ -223,4 +226,51 @@ void ScheduleMicrotask(Dart_NativeArguments args) { | |||
UIDartState::Current()->ScheduleMicrotask(closure); | |||
} | |||
|
|||
void LookupClosure(Dart_NativeArguments args) { |
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.
This code will probably propagate error handles correctly, but I like to check Dart_Handle return values with DART_CHECK_VALID() unless there's some reason not to.
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.
Done.
lib/ui/plugins.dart
Outdated
|
||
part of dart.ui; | ||
|
||
abstract class PluginUtilities { |
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.
Is it possible to write tests of these methods? If so, I think we should have some.
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.
Yes, it should be. I'll go ahead and write some.
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.
Tests added in latest commit.
lib/ui/dart_runtime_hooks.cc
Outdated
|
||
void LookupClosureLibrary(Dart_NativeArguments args) { | ||
Dart_Handle closure = Dart_GetNativeArgument(args, 0); | ||
if (Dart_IsClosure(closure)) { |
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.
If the incoming arg closure is not a closure, don't you need a check that it is a Function Object? further down you are calling Dart_FunctionOwner on it and this method will return an error if it is not a function object.
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.
Yes, good catch. I'll add a check.
lib/ui/dart_runtime_hooks.cc
Outdated
url = Dart_LibraryUrl(owner); | ||
break; | ||
} | ||
} while (!Dart_IsLibrary(owner)); |
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.
Why do you need this loop, if the owner is not a library object highly likely you are not going to get a library object by looping. This code only works for top level functions. For others you need to get the Class object and then call classobj.library() on it.
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.
This loop isn't needed and is already removed in the next patch. Wasn't sure that this worked in the first place (hence the TODO) but I've replaced this code since it didn't work.
New changes, PTAL! |
lib/ui/plugins.dart
Outdated
abstract class PluginUtilities { | ||
static Function getClosureByName({String name, Uri libraryUri, String className}) { | ||
/// Get a closure instance for a specific static or top-level function given |
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.
This should warn that it will fail to find the closure and return null if the closure is not otherwise used in the program. That is, it may get tree-shaken away in an AOT build.
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.
Done.
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.
lgtm
Waiting on review from @cbracken (should be done today) and for this SDK change to be rolled into the engine. |
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.
Dart comments attached. Taking a look at Obj-C next.
lib/ui/plugins.dart
Outdated
/// | ||
/// **Warning:** if the function corresponding with `name` is not referenced | ||
/// explicitly (e.g., invoked, passed as a closure, etc.) it may be optimized | ||
/// out of the compiled application. To prevent this, avoid hard coding names |
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.
nit: hardcoding
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.
Done.
lib/ui/plugins.dart
Outdated
/// retrieve the function's name. | ||
/// | ||
/// `name` is the name of the function we want a closure for. This is a | ||
/// required parameter. |
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.
Typical wording is 'This must not be null.'
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.
Done.
lib/ui/plugins.dart
Outdated
/// path for. | ||
/// | ||
/// Returns a [String] representing the path to the library which contains | ||
/// `closure`. Returns `null` if an error occurs or `closure` is not found |
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.
Trailing period.
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.
Done.
lib/ui/plugins.dart
Outdated
/// Get the path for the library which contains a given method. | ||
/// | ||
/// `closure` is the closure of the function that we want to find the library | ||
/// path for. |
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.
Add: 'This must not be null.'
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.
Done.
lib/ui/plugins.dart
Outdated
/// Get the name of a [Function] as a [String]. | ||
/// | ||
/// Returns a [String] representing the name of the function if the function | ||
/// exists, otherwise `null` is returned. |
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.
Document that closure must not be null.
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.
Done.
lib/ui/plugins.dart
Outdated
/// Returns a [String] representing the name of the function if the function | ||
/// exists and is a member of a class, otherwise `null` is returned. | ||
static String getNameOfFunctionClass(Function closure) { | ||
if(closure == null) { |
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.
nit: if (closure == null) {
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.
Done.
lib/ui/plugins.dart
Outdated
static Function getClosureByName({String name, String libraryPath, String className}) { | ||
if (name == null) { | ||
throw new ArgumentError.notNull('name'); | ||
} |
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.
Here and elsewhere: for null checks, generally prefer: assert(name != null);
We typically reserve ArgumentError for malformed values -- e.g. we're passed a list of length 3 when we document it must be 4, etc.
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.
Done.
lib/ui/plugins.dart
Outdated
|
||
/// Get the name of a class containing [Function] as a [String]. | ||
/// | ||
/// `closure` must not be `null. |
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.
Missing a close quote on null
. Generally speaking though, we don't offset 'null' in monospace, so I'd remove them here and elsewhere in this patch.
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.
Done!
@@ -166,6 +166,11 @@ + (instancetype)methodChannelWithName:(NSString*)name | |||
autorelease]; | |||
} | |||
|
|||
- (void)setBinaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger { | |||
[_messenger release]; | |||
_messenger = [messenger retain]; |
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.
If _messenger
and messenger
are the same object this may result in the object being freed. Typically add a guard to avoid this:
if (_messenger != messenger) {
[_messenger release];
_messenger = [messenger retain];
}
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.
Actually, this method doesn't need to be here anymore. Must have forgotten to remove it.
Blocked until this change is made available in the engine after the Dart SDK roll. |
lib/ui/plugins.dart
Outdated
part of dart.ui; | ||
|
||
/// Functionality for Flutter plugin authors. | ||
abstract class PluginUtilities { |
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.
can you talk about why you need these?
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.
These are needed to lookup callback methods which are top-level functions or static methods in a class by name. The general flow is something like this:
- Plugin user passes callback
fooBar
to the plugin. - The plugin calls
PluginUtilities.getPathForFunctionLibrary
,PluginUtilities.getNameOfFunction
, andPluginUtilities.getNameOfFunctionClass
to get the library URL, function name, and class name (if applicable). These values are passed from native as strings to a callback handler (this is a Dart method) setup by the plugin when a background event is triggered (e.g.,AlarmManager
fires). - When in the callback handler is invoked,
PluginUtilities.getClosureByName
is called with the function name, class name, and library path to retrieve the closure for the plugin user's callback. The plugin can then invoke said closure.
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.
I am concerned about this API. From most serious to least serious concerns:
-
If someone uses this API directly with input that is in any way under the control of the user, it's a ripe for becoming an attack vector, where the user causes the app to jump to arbitrary top-level static functions.
-
This seems like it will be abused. For example, someone might store this data, pass it over the network to another instance of the program, then look up the address to jump to and do that. This would be very sketchy code, but this API doesn't make it feel bad.
-
What happens if someone caches these values across version updates, then uses them in a subsequent version of their code where the function has been tree-shaken out?
-
Least serious: if we have to have this API, it seems like we should have a single type that represents the path, name, and class of the closure.
Can you talk more broadly about what problem these methods solve? Maybe we can find a safer solution to this problem.
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.
Good point. I feel like a lot of your concerns could be addressed by an opaque class with static methods that could:
- Create an instance of the class given a closure which contains all the path, name, and class information.
- Take in an instance of this class and return the closure it references.
This way, we can avoid having these strings visible and modifiable by the user, prevent abuse of what are basically Dart VM APIs, and make caching difficult. The only difficulty I see with this approach is passing said object over method channels since it would have to be serialized and deserialized in such a way that the data itself still remains private, but anyone can listen to a method channel if they know the name of the channel. I'd have to give this part some thought...
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.
Your latest update resolves my concerns. Thanks!
lib/ui/plugins.dart
Outdated
/// [PluginUtilities.getCallbackFromHandle] to retrieve a tear-off of the | ||
/// original callback. If `callback` is not a top-level or static function, | ||
/// null is returned. | ||
static int getCallbackHandle(Function callback) { |
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.
rather than return an int, you could return an object with a single field that's an int. That way you get better type checking and the APIs end up self-documenting more.
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.
The main reason I hadn't done that in the first place was that the object would need to be serialized and then deserialized by the plugin author when being passed over the method channels. If that's not a problem, I'll go ahead and add described object.
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.
Well they're going to have to serialize and deserialize the int, it's barely any more effort for them to do that using the int property of an object they're holding instead of just an int they're holding.
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.
Fair enough. Added in the latest commit.
PluginUtilities.getCallbackHandle
CLAs look good, thanks! |
Added sentences stating that objects returned from getCallbackHandle and getCallbackFromHandle should be cached to avoid potentially expensive lookups.
Dart method `PluginUtilities.getCallbackHandle`. | ||
- Returns: A FlutterCallbackInformation object which contains the name of the | ||
callback, the name of the class in which the callback is defined, and the | ||
path of the library which contains the callback. |
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.
It looks like this returns a triple of empty strings if the handle is bogus. Should it return nullptr instead?
lib/ui/plugins/callback_cache.cc
Outdated
if (iterator != cache_.end()) { | ||
return iterator->second; | ||
} | ||
return {"", "", ""}; |
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.
Should this returns something else to more clearly indicate the error?
lib/ui/plugins.dart
Outdated
/// original callback. If `callback` is not a top-level or static function, | ||
/// null is returned. | ||
/// | ||
/// The result of this method should be cached in order to avoid repeated |
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.
Oh, I guess you could add a cache in this class.
static Map<Function, int> _forwardCache;
static Map<int, Function> _backwardCache;
getCallbackHandle(Function callback) {
// return if it's in _forwardCache
// otherwise do the _getCallbackHandle and add to both _forwardCache and _backwardCache
}
and similar for getCallbackFromHandle.
analysis_options.yaml
Outdated
@@ -42,7 +42,7 @@ linter: | |||
# - avoid_bool_literals_in_conditional_expressions # not yet tested | |||
# - avoid_catches_without_on_clauses # we do this commonly | |||
# - avoid_catching_errors # we do this commonly | |||
- avoid_classes_with_only_static_members | |||
# - avoid_classes_with_only_static_members # required for PluginUtilities caching / lookup methods |
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.
I think we'd have to check with @Hixie and/or @tvolkert , but I wonder if the intention here is to encourage use of the pattern of using singleton instances instead of all-static classes to make it easier to create mocks for testing, etc.. So instead of PluginUtilities being all-static, there would be one global instance of it set up when the framework is initialized.
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.
Please don't comment out this line, though feel free to add additional comments based on the following to explain why this is set:
We want to avoid classes that can be instantiated or extended but only have statics, as in:
class A { static void test() { } }
// ...
A foo = new A(); // what is this??
// ...
class B extends A { } // what is this??
The way we avoid allowing this to be instantiated is to have an invalid private factory constructor. The pattern is:
class A {
// This class is only a namespace, and should not be instantiated or
// extended directly.
factory A._() => null;
void test() { }
}
Adding this will silence the warning.
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.
lgtm
Fixes iOS side of issue #3671.
An example plugin receiving location updates in a background isolate can be found here.