Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Background Execution Implementation for iOS #5539

Merged
merged 20 commits into from
Jul 13, 2018
Merged

Background Execution Implementation for iOS #5539

merged 20 commits into from
Jul 13, 2018

Conversation

bkonyi
Copy link
Contributor

@bkonyi bkonyi commented Jun 15, 2018

Fixes iOS side of issue #3671.

An example plugin receiving location updates in a background isolate can be found here.

@implementation FlutterHeadlessDartRunner {
shell::ThreadHost _threadHost;
std::unique_ptr<shell::Shell> _shell;
}

- (void)runWithEntrypoint:(NSString*)entrypoint {
- (instancetype)init {
Copy link
Member

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?

Copy link
Contributor Author

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.

library = Dart_LookupLibrary(library_name);
DART_CHECK_VALID(library);
}
Dart_Handle closure = Dart_GetClosure(Dart_RootLibrary(), closure_name);
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, will fix.

}
Dart_Handle owner = closure;
Dart_Handle url = Dart_Null();
// TOOD(bkonyi): do we need to do this loop?
Copy link
Member

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?

@@ -223,4 +226,51 @@ void ScheduleMicrotask(Dart_NativeArguments args) {
UIDartState::Current()->ScheduleMicrotask(closure);
}

void LookupClosure(Dart_NativeArguments args) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.


part of dart.ui;

abstract class PluginUtilities {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.


void LookupClosureLibrary(Dart_NativeArguments args) {
Dart_Handle closure = Dart_GetNativeArgument(args, 0);
if (Dart_IsClosure(closure)) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

url = Dart_LibraryUrl(owner);
break;
}
} while (!Dart_IsLibrary(owner));
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@bkonyi
Copy link
Contributor Author

bkonyi commented Jun 20, 2018

New changes, PTAL!

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
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Member

@zanderso zanderso left a comment

Choose a reason for hiding this comment

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

lgtm

@bkonyi
Copy link
Contributor Author

bkonyi commented Jun 20, 2018

Waiting on review from @cbracken (should be done today) and for this SDK change to be rolled into the engine.

Copy link
Member

@cbracken cbracken left a 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.

///
/// **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
Copy link
Member

Choose a reason for hiding this comment

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

nit: hardcoding

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// retrieve the function's name.
///
/// `name` is the name of the function we want a closure for. This is a
/// required parameter.
Copy link
Member

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.'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// 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
Copy link
Member

Choose a reason for hiding this comment

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

Trailing period.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// 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.
Copy link
Member

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.'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// 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.
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// 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) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: if (closure == null) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

static Function getClosureByName({String name, String libraryPath, String className}) {
if (name == null) {
throw new ArgumentError.notNull('name');
}
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.


/// Get the name of a class containing [Function] as a [String].
///
/// `closure` must not be `null.
Copy link
Member

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.

Copy link
Contributor Author

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];
Copy link
Member

@cbracken cbracken Jun 25, 2018

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];
}

Copy link
Contributor Author

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.

@cbracken
Copy link
Member

lgtm

@bkonyi
Copy link
Contributor Author

bkonyi commented Jun 28, 2018

Blocked until this change is made available in the engine after the Dart SDK roll.

part of dart.ui;

/// Functionality for Flutter plugin authors.
abstract class PluginUtilities {
Copy link
Contributor

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?

Copy link
Contributor Author

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, and PluginUtilities.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.

Copy link
Contributor

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.

Copy link
Contributor Author

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...

Copy link
Contributor

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!

/// [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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@googlebot
Copy link

CLAs look good, thanks!

@flutter flutter deleted a comment from googlebot Jul 11, 2018
@flutter flutter deleted a comment from googlebot Jul 11, 2018
@flutter flutter deleted a comment from googlebot Jul 11, 2018
bkonyi added 6 commits July 11, 2018 13:06
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.
Copy link
Member

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?

if (iterator != cache_.end()) {
return iterator->second;
}
return {"", "", ""};
Copy link
Member

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?

/// 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
Copy link
Member

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.

@@ -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
Copy link
Member

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.

Copy link
Contributor

@Hixie Hixie Jul 12, 2018

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.

Copy link
Member

@zanderso zanderso left a comment

Choose a reason for hiding this comment

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

lgtm

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants