[dart-announce] BREAKING CHANGE: Strong Zones

58 views
Skip to first unread message

'Florian Loitsch' via Dart Announcements

unread,
Sep 20, 2017, 1:17:41 PM9/20/17
to announce

With Dart 2.0-dev.1.0 (the release after 2017-09-20) Dart's zones are finally becoming strong-mode clean. Unfortunately, this update introduces some breaking changes to the Zone's API.


This mail discusses the changes to the API and how users can adapt their code to make it work.


The corresponding CL is https://dart-review.googlesource.com/c/sdk/+/4942

It was committed as https://github.com/dart-lang/sdk/commit/38bf70d7ac4ea695adc65ae3d7d0fa083b4dcd46


TL;DR

There were breaking changes to the Zone API of dart:async. If you implement or use zones, please update your code (if necessary). See the "How to Fix" section below for more details.


Zone API

In Dart, zones from dart:async are used to provide an environment that stays stable across asynchronous calls. Zones can furthermore wrap methods that are registered as asynchronous event handlers. This allows users to execute actions when an asynchronous operation returns from the event loop. For example, the stack_trace package uses this functionality to collect stracktraces that survive event-loop interactions.


Users can provide "hooks" to zones, by providing callbacks that are invoked when specific actions happen. These actions are operations like registerCallback, run, or even print. In all these cases, core library code calls into the current zone. For example, instead of emitting characters to the screen, the print function invokes Zone.current.print with the correct arguments.


Many interceptable zone operations deal with functions. For example, registerCallback is invoked when a closure is about to be used as an asynchronous callback, and provides the zone the opportunity to wrap the function. There are three different registerX functions that deal with different arities, but the exact function type varies. In Dart 1.x this wasn't a problem, since dynamic could be used. A registerX function could just wrap the original function and return a new function that was taking and returning dynamic instead.


The new strong-mode compliant zone API fixes this, by introducing generic arguments for these callbacks. For instance, the registerCallback function, now takes one generic argument T which defines the return type: T Function() registerCallback<T>(T Function() f). Adding some generic arguments to the Zone functions is non-breaking and is similar to many other strong-mode cleanups.


A Zone.registerCallback may invoke a user-provided callback that should do the work instead. (It's a bit more complicated with ZoneDelegates and other involved classes, but conceptually that's what's happening). This callback needs to be able to do the full work of registerCallback. This means, that users have to provide functions that satisfy the `registerCallback`'s signature:

typedef RegisterCallbackHandler = T Function() Function<T>(T Function() f);

As can be seen, the function type is generic. This was the reason this change could not be done at the same time as all the other strong-mode cleanups: generic closure types were not supported at that time.


Most of the Zone changes consist of adding the missing generic type informations to the API. Users of the Zone API need to update their hooks so that they are generic as well.

Breaking Changes

Most of the breakages are introduced because of the additional typing information. Almost all users need to update their code to use the generic arguments for their hooks.


However, during the cleanup we also discovered some functions that didn't work well in a fully typed system: handleUncaughtError, runZoned, runGuarded and bindCallback.


The handleUncaughtError function is invoked when an error is not handled and should be reported. (It is also invoked when an error tries to cross an error-boundary).


It was typed as R handleUncaughtError<R>(error, StackTrace trace), but it became obvious pretty fast, that functions are generally unable to satisfy the return-type requirement except by returning null. As such, the function was changed to be a void function instead.


This change impacts all functions that are supposed to return a value when an error occurs: runZoned, runGuarded and bindCallback (when its optional argument runGuarded is true). All of these catch synchronous errors and invoke the error handler when this happens. Since the error handler doesn't provide any value anymore, their functionality needed to be reworked.


If runZoned is invoked with an error-handler, than all errors (synchronous or not) are given to the provided handler. In the synchronous case, the return value of the onError handler would be used as the result of runZoned. This is not the case anymore: since onError is a void function now, it simply returns null when there is a synchronous error.


The runGuarded function basically puts a try/catch around the provided closure and invokes it. In case of a synchronous error, it invokes the zone's handleUncaughtError. Since this one can't return a value of the correct type anymore, runGuarded was changed to only take void functions, and to have a void return value.


Similarly, bindCallback was a combination of registerCallback and runGuarded if the named argument runGuarded was true (its default value). For the same reasons as above, this didn't make any sense anymore. The bindCallback functions were thus split into two:

  • bindCallback which doesn't run guarded.

  • bindCallbackGuarded which only takes void functions and invokes the bound callback in a guarded way.

How To Fix

This change affects users in different ways:

  1. Type issues

  2. API changes


The typing issues have to be resolved the same way as every other strong-mode change. Generally it involves adding new generic types. For example, the CL for the `observe` package consists of simply adding the necessary generic types (slightly trimmed):


--- a/lib/src/dirty_check.dart

+++ b/lib/src/dirty_check.dart

@@ -107,7 +107,8 @@ ZoneSpecification dirtyCheckZoneSpec() {


-  Func0 wrapCallback(Zone self, ZoneDelegate parent, Zone zone, f()) {

+  ZoneCallback<R> wrapCallback<R>(

+      Zone self, ZoneDelegate parent, Zone zone, R f()) {

    if (f == null) return f;

@@ -116,7 +117,8 @@ ZoneSpecification dirtyCheckZoneSpec() {


-  Func1 wrapUnaryCallback(Zone self, ZoneDelegate parent, Zone zone, f(x)) {

+  ZoneUnaryCallback<R, T> wrapUnaryCallback<R, T>(

+      Zone self, ZoneDelegate parent, Zone zone, R f(T x)) {

                                                                                   


These changes are backwards-compatible.


The API changes may require more work, and may not be backwards-compatible. Package authors should either update their SDK constraint to ">=2.0.0-dev.1.0", or test their packages with the 1.24 and the 2.0-dev releases.

runZoned

Most of the time, the runZoned is invoked for code that is not supposed to throw synchronously. This means, that code that uses runZoned is most often unaffected.


If the onError handler visibly returns something that is supposed to be used synchronously, the code has to be rewritten. For example, as follows:


// Before:

int count = runZoned(() {

 startAsyncOperation();

 return 5 ~/ maybeZeroVariable;

}, onError: (e, s) {

 print("caught error: $e");

 return 0;

});


// After:

bool wasError = false;

int count = runZoned(() {

 startAsyncOperation();

 return 5 ~/ maybeZeroVariable;

}, onError: (e, s) {

 print("caught error: $e");

 wasError = true;

});

if (wasError) count = 0;


Note that this transformation is safe to use for the old API as well.

bindCallback

The bindCallback functions (bindCallback, bindUnaryCallback, bindBinaryCallback) were significantly changed. They don't take any optional named argument anymore, and the default behavior for not providing an argument changed.


The easiest case is, when users passed the runGuarded flag and set it to `false`. In this case, the transition to the new API consists of simple removing the named argument:


// Before:

var g = zone.bindCallback(f, runGuarded: false);


// After:

var g = zone.bindCallback(f);


// Alternative After:

var capturedZone = zone;

var registered = capturedZone.registerCallback(f);

var g = () => capturedZone.run(registered);


The alternative "after" is a bit longer, but has the advantage that it works for the old and the new API in the same way.


If the code didn't set runGuarded to `false` then the code needs to make sure that the provided function is a `void` function and transform as follows:


// Before:

var f = zone.bindCallback(f);


// After:

var f = zone.bindCallbackGuarded(f);


// Alternative After:

var capturedZone = zone;

var registered = capturedZone.registerCallback(f);

var g = () { capturedZone.runGuarded(registered); };


Again the "alternative" option has the advantage that it works the same way before and after the change.


If the provided function is *not* a void function, then the transition is dependent on the context. Feel free to contact me (floitsch@) for advice.

runGuarded

The runGuarded function (not the same-named flag to bindCallback) runs code synchronously and forwards errors to the zone's error handler. The API change consists of only taking `void` functions. This is very similar to `runZoned` and requires similar fixes.


Example:

// Before. The value of `runGuarded` is used.

var x = zone.runGuarded(() => 499);


// After: `runGuarded` is a `void` function.

var x;

zone.runGuarded(() { x = 499; });

if (x == null) {

 // Something happened synchronously. Was probably not dealt with before either.

 // Often this case is not even possible.

}


Note that this transformation works for both the old and new API.


--
For more news and information, visit https://plus.google.com/+dartlang
 
To join the conversation, visit https://groups.google.com/a/dartlang.org/

'Kevin Moore' via Dart Announcements

unread,
Sep 20, 2017, 1:30:52 PM9/20/17
to Dart Announcements
FYI: 2.0.0-dev.1.0 has not been released (yet). These changes are currently in bleeding edge and they will be part of -dev.1.0 – which we expect with a week.
Reply all
Reply to author
Forward
0 new messages