Skip to content
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

[Breaking Change Request] Declare return types of Uint8List #36900

Closed
tvolkert opened this issue May 8, 2019 · 62 comments
Closed

[Breaking Change Request] Declare return types of Uint8List #36900

tvolkert opened this issue May 8, 2019 · 62 comments
Assignees
Labels
area-sdk Use area-sdk for general purpose SDK issues (packaging, distribution, …). breaking-change-request This tracks requests for feedback on breaking changes

Comments

@tvolkert
Copy link
Contributor

tvolkert commented May 8, 2019

Current state

The following methods all return Uint8List, yet they are only declared to return List<int>:

  • Utf8Codec.encode()
  • BytesBuilder.takeBytes()
  • BytesBuilder.toBytes()
  • File.readAsBytes()
  • File.readAsBytesSync()
  • RandomAccessFile.read()
  • RandomAccessFile.readSync()
  • Uint8List.sublist()

Relatedly, the following sublist() methods return sublists of the same type as
the source list, yet they only declare that they return the more generic type (e.g. List<int>, List<float>, or List<double>):

  • Int8List
  • Uint8ClampedList
  • Int16List
  • Uint16List
  • Int32List
  • Uint32List
  • Int64List
  • Uint64List
  • Float32List
  • Float64List
  • Float32x4List
  • Int32x4List
  • Float64x2List

Intended change

This issues proposes to update the API of the aforementioned methods to declare the return value of the more specific return type (e.g. Uint8List rather than List<int>).

Rationale

  1. Better type safety

    Callers would like to statically prove that you can obtain a ByteBuffer from the result of these API calls.

  2. The current API/implementation combo encourages bad practices.

    There are two dangers with an API saying it returns List<int> and always returning Uint8List:

    1. People do roundabout things to guarantee that the result is a Uint8List (such as defensively wrapping the result in a Uint8List), which causes unnecessary overhead.
    2. People start to depend on the result being a Uint8List and automatically performing a contravariant cast (which is already happening in Flutter and elsewhere) -- and if anyone new implements the interface and returns something other than a Uint8List, code breaks.
  3. To match the documentation

    Utf8Encoder and Base64Decoder.convert, for instance, already document that they return Uint8List.

See also:

Expected impact

On callers

Callers of these APIs will not be impacted at all since the new return types are subtypes of the existing return types (and moreover, the return values will be the exact same values).

On implementations of the interfaces

Libraries that are implementing the following interfaces will be broken because they will no longer be implementing the interface:

  • Utf8Encoder
  • BytesBuilder
  • File
  • RandomAccessFile
  • Int8List
  • Uint8List
  • Uint8ClampedList
  • Int16List
  • Uint16List
  • Int32List
  • Uint32List
  • Int64List
  • Uint64List
  • Float32List
  • Float64List
  • Float32x4List
  • Int32x4List
  • Float64x2List

This includes (but is not limited to) some well-known packages, such as package:typed_data and package:file.

Steps for mitigation

For known affected packages, the plan will be to issue patch releases to those packages that tighten the SDK constraints to declare that the current version of the package is not compatible with an SDK version greater than the current dev version. Then, once the change lands, we'll update the affected packages to implement the new API and loosen their SDK constraints once again.

@tvolkert tvolkert added area-sdk Use area-sdk for general purpose SDK issues (packaging, distribution, …). breaking-change-request This tracks requests for feedback on breaking changes labels May 8, 2019
@lexaknyazev
Copy link
Contributor

For better consistency across API, maybe some other methods could be similarly updated?

  • File: Stream<List<int>> openRead to Stream<Uint8List> openRead.
  • RawSocket: List<int> read([int len]) to Uint8List read([int len]).
  • Socket implements Stream<List<int>>, IOSink to Socket implements Stream<Uint8List>, IOSink, so that each onData event is Uint8List.
  • List<int> Datagram.data field to Uint8List Datagram.data.
  • Stdin extends _StdStream implements Stream<List<int>> to Stdin extends _StdStream implements Stream<Uint8List>
  • ZLibEncoder.convert and ZLibDecoder.convert actually return Uint8List.

Main use-case - accessing buffer property of received binary chunks, e.g. for creating views.

@tvolkert
Copy link
Contributor Author

tvolkert commented May 9, 2019

While I support those changes, the stream changes would be much more invasive, because they would break callers (not just interface implementors). For example, the following would no longer be valid:

await for (List bytes in file.openRead()) {}

Datagram.data would likewise affect callers since the data property is read/write and settable in the constructor.

The other changes you suggest should be able to be included in this proposal with the same impact. Specifically:

  • RawSocket.read()
  • ZLibEncoder.convert()
  • ZLibDecoder.convert()
  • (edit) File.openRead()
  • (edit) Socket
  • (edit) Stdin

@lrhn
Copy link
Member

lrhn commented May 9, 2019

A Uint8List is-a List<int>, so await for (List<int> bytes in file.openRead()) { ... } would still be valid. The Uint8Lists will be assignable to bytes.

The places you should be concerned about are the ones where the Uint8List occurs contravariantly, like a void add(List<int> data) method found in a few places in, e.g. and dart:convert (conversion sinks in general). In that situation, users passing in a List<int> will be broken if we change it to Uint8List.
We also don't need to, it's quite possible to have a Converter<String, List<int>> with a ConversionSink<String, List<int>> which accepts List<int> as arguments, and still declare a return type of Uint8List where possible.
That is, only change covariantly occurring List<int>s to Uint8List. That should be a "safe" change (except for other code implementing the interface).

Since streams are entirely covariant, I don't think changing Stream<List<int>< (or Future<List<int>> if we have any) to Uint8List should be a problem.

@tvolkert
Copy link
Contributor Author

tvolkert commented May 9, 2019

Good point - I think I was squinting too hard when I was thinking about the streams cases 😄

@lexaknyazev
Copy link
Contributor

Just found two more streams: Process.stdout and Process.stderr.

@Hixie
Copy link
Contributor

Hixie commented May 10, 2019

There's no reason to change the contravariant case right? The reason to do this is just that we can guarantee that places where we want a Uint8List (which is more efficient than a List of int) we definitely get one.

@tvolkert
Copy link
Contributor Author

@Hixie correct.

@aadilmaan
Copy link
Contributor

@dgrove @matanlurey for approval.
I see @Hixie is already engaged =).

@jakemac53
Copy link
Contributor

I would like to discuss the mitigation strategy more. I don't think there is necessarily anything better that we can do, but the suggested strategy does have shortcomings that should be highlighted.

For known affected packages, the plan will be to issue patch releases to those packages that tighten the SDK constraints to declare that the current version of the package is not compatible with an SDK version greater than the current dev version. Then, once the change lands, we'll update the affected packages to implement the new API and loosen their SDK constraints once again.

Releasing a new version with a tightened sdk constraint doesn't prevent users from getting an older version of the package which still has the wider constraint. This leads to a few issues:

  • The second we release the new sdk, a pub upgrade or pub get will happily just downgrade the old package version (which will be incompatible).
  • Even once the new package is available, if it can't be selected for some other reason, then users will still get the old, incompatible version.

There are couple things I can think of to mitigate this, but we can't solve the fundamental problem afaik (at least not without some really ugly pub hacks):

  • Release the versions with the restrictive upper bounds as early as possible, with as large of a time window as possible before the sdk release. The ideal scenario is people update to that version before the sdk drops, which means when they get the new sdk they are forced to do a pub upgrade, and get the new version.
    • If they are still on an older version (that allows the new sdk), then I believe pub will think everything is still OK and won't prompt them to upgrade packages.
  • Make sure that the packages don't do breaking version bumps for this update, even though it could be considered breaking. This mitigates issues around version constraints on the packages holding people back to older (incompatible) versions.
  • Release the new versions of packages before the SDK is actually available. Users simply won't be able to get it until that sdk is released, but as soon as it is they will get it. It might be difficult to validate these versions though, before an sdk is released. Also they would probably get dinged on their pub score for being "broken". Ideally, we actually handle this rollout in the dev channel.

@jakemac53
Copy link
Contributor

Also for posterity the "really ugly pub hacks" would involve either patching the pubspecs of the uploaded versions of these packages (to limit the sdk upper bound) or adding custom logic in the pub client itself to artificially restrict the older versions of these packages.

@tvolkert
Copy link
Contributor Author

Actually, it occurs to me that the affected packages could release new versions now that return the more specific return types, and they'll still be properly implementing the interfaces as currently defined.

@matanlurey
Copy link
Contributor

This is LGTM from me. @jakemac53 In general I agree with you, but I don't think it applies to this change, ~mostly, with maybe the only exception being:

// Before
var results = [file.readAsBytesSync()]; 
results.add([0]); // OK

// After
var results = [file.readAsBytesSync()]; 
results.add([0]); // Runtime error: List<int> is not a type of UInt8List

@jakemac53
Copy link
Contributor

I agree this won't affect most consumers code directly, but we need to make sure they fetch a valid version of their dependencies that are affected, which is the part that concerns me.

Even if that is a very small number of packages, it sounds like they are fairly core packages (typed_data specifically) that are affected so done wrong this could cause a lot of pain.

@tvolkert
Copy link
Contributor Author

@jakemac53 following on your suggestions, what if we never mucked with the SDK constraints of packages at all, but rather released new versions of the affected packages (that contain the restrictive upper bounds) ASAP without a breaking version bump, so that when the breaking change lands, anyone who's affected should be able to get into a good state by simply running pub upgrade? Only packages that pin to a specific patch release of a package will be forced to do anything more than that (they'll be forced to pin to a newer version).

Thoughts?

@tvolkert
Copy link
Contributor Author

tvolkert commented May 20, 2019

So I did a deep dive on all affected packages, and it turns out package:typed_data is not affected.

Here are the rollout implementor changes so far:

tvolkert added a commit to tvolkert/bazel_worker that referenced this issue May 20, 2019
tvolkert added a commit to tvolkert/bazel_worker that referenced this issue May 20, 2019
@tvolkert
Copy link
Contributor Author

Ok, I think I've found all affected implementors (linked in the PRs above). The scope of the potential breakage is much more limited than it could have been.

@rdev-software
Copy link

I think my project face issue related to this change:

Compiler message:
file:///Users/tr/development/flutter/.pub-cache/hosted/pub.dartlang.org/servicestack-1.0.10/lib/client.dart:450:46: Error: The argument type 'Utf8Decoder' can't be assigned to the parameter type 'StreamTransformer<Uint8List, dynamic>'.
 - 'Utf8Decoder' is from 'dart:convert'.
 - 'StreamTransformer' is from 'dart:async'.
 - 'Uint8List' is from 'dart:typed_data'.
Try changing the type of the parameter, or casting the argument to 'StreamTransformer<Uint8List, dynamic>'.
      var bodyStr = await res.transform(utf8.decoder).join();
                                             ^
file:///Users/tr/development/flutter/.pub-cache/hosted/pub.dartlang.org/servicestack-1.0.10/lib/client.dart:462:58: Error: The argument type 'Utf8Decoder' can't be assigned to the parameter type 'StreamTransformer<Uint8List, dynamic>'.
 - 'Utf8Decoder' is from 'dart:convert'.
 - 'StreamTransformer' is from 'dart:async'.
 - 'Uint8List' is from 'dart:typed_data'.
Try changing the type of the parameter, or casting the argument to 'StreamTransformer<Uint8List, dynamic>'.
      var jsonObj = json.decode(await res.transform(utf8.decoder).join());
                                                         ^
file:///Users/tr/development/flutter/.pub-cache/hosted/pub.dartlang.org/servicestack-1.0.10/lib/client.dart:513:49: Error: The argument type 'Utf8Decoder' can't be assigned to the parameter type 'StreamTransformer<Uint8List, dynamic>'.
 - 'Utf8Decoder' is from 'dart:convert'.
 - 'StreamTransformer' is from 'dart:async'.
 - 'Uint8List' is from 'dart:typed_data'.
Try changing the type of the parameter, or casting the argument to 'StreamTransformer<Uint8List, dynamic>'.
    return json.decode(await res.transform(utf8.decoder).join());
                                                ^
file:///Users/tr/development/flutter/.pub-cache/hosted/pub.dartlang.org/servicestack-1.0.10/lib/client.dart:537:45: Error: The argument type 'Utf8Decoder' can't be assigned to the parameter type 'StreamTransformer<Uint8List, dynamic>'.
 - 'Utf8Decoder' is from 'dart:convert'.
 - 'StreamTransformer' is from 'dart:async'.
 - 'Uint8List' is from 'dart:typed_data'.
Try changing the type of the parameter, or casting the argument to 'StreamTransformer<Uint8List, dynamic>'.
      String str = await res.transform(utf8.decoder).join();
Flutter (Channel dev, v1.8.2, on Mac OS X 10.14.4 18E226, locale en-NZ)
    • Flutter version 1.8.2 at /Users/tr/development/flutter
    • Framework revision 0a39d8d92e (2 weeks ago), 2019-07-10 19:17:14 -0700
    • Engine revision 75387dbc14
    • Dart version 2.5.0 (build 2.5.0-dev.0.0 b5aeaa6796)

Any idea how to fix it ?

@whesse
Copy link
Contributor

whesse commented Jul 26, 2019

The problem with a transformer having a less specific type than the stream it is transforming can be fixed by changing
res.transform(utf8.decoder)
to the equivalent call
utf8.decoder.bind(res)

This way, the fact that a stream can be transformed by a transformer with a more general type (violating the substitution principle) is changed to the fact that a transformer can transform a stream of a more specific type (the normal behavior for parameters and expressions).

@rdev-software
Copy link

rdev-software commented Jul 26, 2019 via email

@tvolkert
Copy link
Contributor Author

@rdev-software this has already been fixed in package:servicestack, but it looks like they haven't published a new version to Pub yet. Until they do, you can get the fix by putting the following at the bottom of your pubspec.yaml file:

dependency_overrides:
  servicestack:
    git:
      url: git://github.com/ServiceStack/servicestack-dart
      ref: d0f26ff319271b971612268951a75158dcf6641e

@tvolkert
Copy link
Contributor Author

@rdev-software I asked the owner of package:servicestack to publish a new version, which they did -- so now you can just use servicestack: ^1.0.11

johnsonmh pushed a commit to johnsonmh/flutter that referenced this issue Jul 30, 2019
* Prepare for HttpClientResponse Uint8List SDK change

An upcoming change in the Dart SDK will change `HttpClientResponse`
from implementing `Stream<List<int>>` to instead implement
`Stream<Uint8List>`.

This forwards-compatible change to `_MockHttpClientResponse` is being
made to allow for a smooth rollout of that SDK breaking change. The
current structure of the class is as follows:

```dart
_MockHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
  ...
}
```

This structure would require that the Dart SDK change land atomically
a change to the class (`extends Stream<Uint8List>`). This atomic landing
requirement doesn't play well at all with Flutter's roll model vis-a-vis
the Dart SDK's roll model to Google's internal repo.  As such, this commit
changes the structure of `_MockHttpClientResponse` to be:

```dart
_MockHttpClientResponse implements HttpClientResponse {
  final Stream<Uint8List> _delegate;

  ...
}
```

Once the Dart SDK change has fully rolled out, we can simplify this class
back to its former structure.

dart-lang/sdk#36900

* Review comment
johnsonmh pushed a commit to johnsonmh/flutter that referenced this issue Jul 30, 2019
Follow-on change to flutter#34863 (see that change
for context), whereby we ensure that we're properly dealing in `Uint8List`.

These necessary changes would have been caught by disabling implicit casts
in our analysis options.

dart-lang/sdk#36900
flutter#13815
johnsonmh pushed a commit to johnsonmh/flutter that referenced this issue Jul 30, 2019
An upcoming change in the Dart SDK changes `Socket` from
implementing `Stream<List<int>>` to implementing `Stream<Uint8List>`.
This forwards-compatible change in flutter_tools prepares for that
Dart SDK change.

dart-lang/sdk#36900
@leafpetersen
Copy link
Member

What is the current status of this? Did any of this end up landing? cc @aadilmaan

@tvolkert
Copy link
Contributor Author

The original proposal landed and is in Dart 2.5.0.

The expanded proposal was mostly backed out - specifically, the File.openRead() and HttpClientResponse change was backed out. The far less invasive changes to HttpRequest
Socket did land.

https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md#core-libraries-1

@mit-mit
Copy link
Member

mit-mit commented Sep 23, 2019

Closing

@mit-mit mit-mit closed this as completed Sep 23, 2019
alexander-doroshko pushed a commit to alexander-doroshko/flutter-intellij that referenced this issue Jan 24, 2020
This updates call sites with forward-compatible fixes for
a recent Dart SDK change that changed the following APIs
to be `Stream<Uint8List>` rather than `Stream<List<int>>`:

* HttpClientResponse
* File.openRead()

dart-lang/sdk#36900
wrenchMither added a commit to wrenchMither/fluwx that referenced this issue Dec 26, 2022
A recent change to the Dart SDK updated `HttpClientResponse`
to implement `Stream<Uint8List>` rather than implementing
`Stream<List<int>>`.

This forwards-compatible chnage updates calls to
`Stream.transform(StreamTransformer)` to instead call the
functionally equivalent `StreamTransformer.bind(Stream)`
API, which puts the stream in a covariant position and
thus causes the SDK change to be non-breaking.

dart-lang/sdk#36900
allmoviestvshowslistsfilmography015 added a commit to allmoviestvshowslistsfilmography015/alice that referenced this issue Aug 29, 2024
A recent change to the Dart SDK updated `HttpClientResponse`
to implement `Stream<Uint8List>` rather than implementing
`Stream<List<int>>`.

This forwards-compatible chnage updates calls to
`Stream.transform(StreamTransformer)` to instead call the
functionally equivalent `StreamTransformer.bind(Stream)`
API, which puts the stream in a covariant position and
thus causes the SDK change to be non-breaking.

dart-lang/sdk#36900
devoncarew pushed a commit to dart-lang/tools that referenced this issue Oct 4, 2024
devoncarew pushed a commit to dart-lang/tools that referenced this issue Oct 4, 2024
An upcoming change to the Dart SDK will change the signature
of `File.openRead()` from returning `Stream<List<int>>` to
returning `Stream<Uint8List>`.

This forwards-compatible change prepares for that SDK breaking
change by casting the Stream to `List<int>` before transforming
it.

dart-lang/sdk#36900
mosuem pushed a commit to dart-lang/http that referenced this issue Oct 17, 2024
…rt-lang/http2#44)

An upcoming change to the Dart SDK will change `HttpRequest` and
`HttpClientResponse` from implementing `Stream<List<int>>` to
implementing `Stream<Uint8List>`.

This forwards-compatible change prepares for that SDK breaking
change by casting the Stream to `List<int>` before transforming
it.

dart-lang/sdk#36900
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-sdk Use area-sdk for general purpose SDK issues (packaging, distribution, …). breaking-change-request This tracks requests for feedback on breaking changes
Projects
None yet
Development

No branches or pull requests