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

More support for wasm #178

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open

Conversation

koji-1009
Copy link
Contributor

@koji-1009 koji-1009 commented Dec 2, 2024

There are several related fixes.

@koji-1009 koji-1009 changed the title More support for wasm environment More support for wasm Dec 2, 2024
Copy link
Member

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

.github/workflows/test.yml Outdated Show resolved Hide resolved
.github/workflows/test.yml Outdated Show resolved Hide resolved
window.crypto.getRandomValues(array.toJS);
return array;
final values = window.crypto.getRandomValues(array.toJS);
return (values as JSUint8Array).toDart;
Copy link
Member

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?

subtle.getRandomValues(destination);
if (destination is Uint8List) {
final newValues = subtle.getRandomValues(destination);
destination.setAll(0, newValues as Uint8List);
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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:
image


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.

Copy link
Contributor Author

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;

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

@jonasfj jonasfj Dec 4, 2024

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

Copy link
Member

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 🤣

test/crypto_subtle_test.dart Outdated Show resolved Hide resolved
} on UnsupportedError {
rethrow;
} on Error catch (e) {
throw _translateJavaScriptException(e);
Copy link
Member

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 🤣

Copy link
Member

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 🤣)

Copy link
Contributor Author

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

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 this is a good idea, we probably have to do that in _handleDomException and in getRandomValues inside lib/src/crypto_subtle.dart.

lib/src/testing/utils/detected_runtime_js_interop.dart Outdated Show resolved Hide resolved
@koji-1009
Copy link
Contributor Author

Thanks for the review. I will make some PRs that can be easily separated and make this PR smaller.

dart-lang/sdk#55496
I was also surprised that the error is different between dart2wasm and dart2js. I would be very happy to have access to JavaScriptError, but it seems difficult.

@koji-1009 koji-1009 marked this pull request as ready for review December 3, 2024 13:45
@koji-1009 koji-1009 requested a review from jonasfj December 3, 2024 14:33
@@ -99,6 +99,13 @@ Object _translateDomException(
'"${e.name}", message: $message');
}

Object _translateJavaScriptException(Error e) {
Copy link
Member

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.

lib/src/impl_js/impl_js.utils.dart Outdated Show resolved Hide resolved
lib/src/crypto_subtle.dart Outdated Show resolved Hide resolved
Comment on lines 339 to 343
final values = array.toJS;
window.crypto.getRandomValues(values);
if (array != values.toDart) {
array.setAll(0, values.toDart);
}
Copy link
Member

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.

Suggested change
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

'array',
'Unsupported TypedData type',
);
throw UnsupportedError('Unsupported TypedData type ${array.runtimeType}');
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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

lib/src/impl_js/impl_js.utils.dart Outdated Show resolved Hide resolved
lib/src/impl_js/impl_js.utils.dart Outdated Show resolved Hide resolved
lib/src/impl_js/impl_js.utils.dart Outdated Show resolved Hide resolved
@@ -68,32 +73,60 @@ void main() {
data.any((e) => e != 0),
isTrue,
);
});
}, skip: 'dart2wasm');
Copy link
Member

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?

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 is because with dart2wasm, the value is not reflected in the UInt8List. All data values are 0.

@koji-1009 koji-1009 requested a review from jonasfj December 10, 2024 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants