-
Notifications
You must be signed in to change notification settings - Fork 47
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
More support for wasm #178
base: master
Are you sure you want to change the base?
Conversation
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.
🚀 Thanks for diving into this, I don't think neither me or @HamdaanAliQuatil has had much time to play with wasm yet.
I'm not sure exactly how to get the error handling, bit right, so let's explore it a bit -- see if there are any related bugs on the issue, etc.
My other suggestion would be to try and split as many of changes into tiny PRs. Those are much easier to land, and it'll make easier for us to focus on the complex issue in this PR.
lib/src/crypto_subtle.dart
Outdated
window.crypto.getRandomValues(array.toJS); | ||
return array; | ||
final values = window.crypto.getRandomValues(array.toJS); | ||
return (values as JSUint8Array).toDart; |
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 there any need for this cast, doesn't getRandomValues
always return the first argument that it is given?
lib/src/impl_js/impl_js.random.dart
Outdated
subtle.getRandomValues(destination); | ||
if (destination is Uint8List) { | ||
final newValues = subtle.getRandomValues(destination); | ||
destination.setAll(0, newValues as Uint8List); |
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 is this good for? getRandomValues
writes to the first argument it is given, does it not?
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 seems that dart2js overwrites the value, but dart2wasm does not. Could you please see that the getRandomValues
test fails once as I will submit a PR later that runs with the --wasm
option in ci?
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.
doesn't that hint at a more fundamental issue.
If you read the documentation for getRandomValues
it's very clear that it fills the first argument with random bytes:
https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
It even says it's updated in-place:
If that's not working when compiling to wasm, then what is happening?
I'd suggest trying to make a minimal example. Because this shouldn't be happening, if we can't pass typedarrays correctly to JS from inside wasm, then something is very wrong.
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 tried it and the following code works.
In the case of dart2js, the array.toJS
reflected the value in the array
, but not in the case of dart2wasm.
final values = array.toJS;
window.crypto.getRandomValues(values);
return values.toDart;
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 we make a gist that contains the smallest possible test case where:
$ dart test -p chrome -c dart2wasm
and
$ dart test -p chrome -c dart2js
behaves differently.
Then let's report a bug against the SDK and see if we can find a workaround.
If there really is a thing where modifications to a Uint8List
passed to Javascript aren't reflected back in Dart that's pretty bad. And we'd need to review the entire code base for all places where we pass something to Javascript to check that we don't rely on side effects.
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.
Ah, I didn't know that .toJS
could return a clone.
We should probably review all places in the code where we do a .toJS
call.
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.
hmm, I guess we could work around this as follows:
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:web/web.dart' as web;
import 'package:test/test.dart';
void main() {
test('calculate', () {
final array = Uint8List(16);
expect(
array.any((e) => e == 0),
isTrue,
);
final jsArray = array.toJS;
web.window.crypto.getRandomValues(jsArray);
// .toJS may cause a clone, so we can't know if
// jsArray.toDart is the same as the original array.
final dartArray = jsArray.toDart;
if (dartArray == array) {
print('We do not need to do anything here');
} else {
array.setAll(0, dartArray);
}
expect(
array.any((e) => e != 0),
isTrue,
);
});
}
probably we can pick some better variable names. But there is no need to always do .setAll
.
And we definitely need a comment explaining that .toJS
will make a clone of the array when we're compiling with dart2wasm.
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 kIsWasm
is a better flag. Because it is a const bool
. I added comments and flags. 7856777
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.
Oops, calling import 'package:flutter/foundation.dart' show kIsWasm;
on crypto_subtle.dart
causes an error.
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.
We can just use bool.fromEnvironment('dart.tool.dart2wasm')
it's the same 🤣
lib/src/impl_js/impl_js.random.dart
Outdated
} on UnsupportedError { | ||
rethrow; | ||
} on Error catch (e) { | ||
throw _translateJavaScriptException(e); |
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'm not necessarily convinced this is a good idea.
We don't want to catch errors that we're not supposed to catch.
Hmm, I'm not sure exactly how we should handle this.
I'm guessing it means that the _handleDomException
logic we have also won't work as intended.
I wonder if it's:
(a) Possible to get access to the JSObject that was thrown (somehow).
(b) Is there a bug for how error handling differs between js and wasm.
I'm betting that we don't have good enough tests in this package for all the exception cases, hence, also why a lot of tests didn't start failing when _handleDomException
doesn't work as expected anymore.
Maybe, we need to put the error handling logic that differs into crypto_suble.dart
such that impl_js/
doesn't have to care whether it's running in dart2js or dart2wasm.
But I'm surprised that error handling differs between js and wasm, maybe we're doing something wrong 🤣
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.
Thanks for referencing dart-lang/sdk#55496
I'd suggest we look at toString()
, because the only error we want to handle here is one that is _JavaScriptError
. We might even want to make this code not run when it's compiled with dart2js (not sure if we can detect wasm vs js, though 🤣)
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 would modify the code as follows 👍
I wasn't expecting Error
handling, so I was confused when implementing it. Thanks for the review.
} on Error catch (e) {
final errorName = e.toString();
if (errorName != 'JavaScriptError') {
rethrow;
}
throw _translateJavaScriptException(e);
}
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 this is a good idea, we probably have to do that in _handleDomException
and in getRandomValues
inside lib/src/crypto_subtle.dart
.
Thanks for the review. I will make some PRs that can be easily separated and make this PR smaller. dart-lang/sdk#55496 |
8ba379b
to
12b0567
Compare
12b0567
to
9539b5f
Compare
lib/src/impl_js/impl_js.utils.dart
Outdated
@@ -99,6 +99,13 @@ Object _translateDomException( | |||
'"${e.name}", message: $message'); | |||
} | |||
|
|||
Object _translateJavaScriptException(Error e) { |
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.
Let's give this a documentation comment.
Let's link to the issue in the SDK, that way when we discover that the issue is resolved, we can work to fix this hack.
27c1bb3
to
ee3ca75
Compare
ee3ca75
to
7856777
Compare
lib/src/crypto_subtle.dart
Outdated
final values = array.toJS; | ||
window.crypto.getRandomValues(values); | ||
if (array != values.toDart) { | ||
array.setAll(0, values.toDart); | ||
} |
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.
Let's avoid calling .toDart
twice, in wasm mode it'll create a wrapper, there is no need to create the wrapper twice.
final values = array.toJS; | |
window.crypto.getRandomValues(values); | |
if (array != values.toDart) { | |
array.setAll(0, values.toDart); | |
} | |
final jsarray = array.toJS; | |
window.crypto.getRandomValues(jsarray); | |
final dartArray = jsarray.toDart; | |
if (array != dartArray) { | |
array.setAll(0, dartArray); | |
} |
See: https://api.dart.dev/dart-js_interop/JSUint8ArrayToUint8List/toDart.html
lib/src/crypto_subtle.dart
Outdated
'array', | ||
'Unsupported TypedData type', | ||
); | ||
throw UnsupportedError('Unsupported TypedData type ${array.runtimeType}'); |
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 are we changing behavior here?
Is this deliberate? If so should it not be orthogonal to this PR, and happen in a separate PR?
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 noticed that when generating unsupported lists (e.g. Uint64List
) in dart2js, an UnsupportedError is thrown. For example, the following test succeeds.
test('Uint64List: not supported type', () {
expect(
() => Uint64List(32),
throwsA(
isA<UnsupportedError>(),
),
);
});
The test fails in dart2wasm.
There is no JS class corresponding to Uint64List
. Therefore, when fillRandomBytes(Uint64List(32))
is called in dart2wasm, the process reaches this code.
I changed it to UnsupportedError to keep the process consistent between dart2js and dart2wasm. It is possible to split a PR.
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 added test case. 4efab3b
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.
And restore previous code. The addition of the test makes it easier to understand the scope of the impact. bd9425a
test/crypto_subtle_test.dart
Outdated
@@ -68,32 +73,60 @@ void main() { | |||
data.any((e) => e != 0), | |||
isTrue, | |||
); | |||
}); | |||
}, skip: 'dart2wasm'); |
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'm curious, why do we need to skip this?
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 is because with dart2wasm, the value is not reflected in the UInt8List. All data
values are 0.
Co-authored-by: Jonas Finnemann Jensen <[email protected]>
Co-authored-by: Jonas Finnemann Jensen <[email protected]>
There are several related fixes.
getRandomValue
, a JavaScriptError will be thrown. However, JavaScriptError is not public. For this reason, the Error class is catched.FixedgetRandomValues
argument values are not updated in the wasm environment, so return value is used.userAgent
via js_interop. This change can also be implemented by introducingpackage:web
.flutter pub run test
twice, the first test for firefox fails the second time. To work around this issue and to reduce CI latency, I am separating the coverage jobs.