fzyzcjy / flutter_rust_bridge

Flutter/Dart <-> Rust binding generator, feature-rich, but seamless and simple.
https://fzyzcjy.github.io/flutter_rust_bridge/
MIT License
4.13k stars 285 forks source link

[Bug] Debug Mode with Web Target #1002

Closed anlumo closed 1 year ago

anlumo commented 1 year ago

Describe the bug

flutter_rust_bridge works when run in profile or release mode on the Web, but not in debug mode. This means that there's no debugging and no hot reload on that target, which makes it pretty much impossible to develop.

The serve utility also only runs in profile and release. I'm currently trying to implement my own solution that allows debugging, but I'm running into problems left right and center.

My idea is to have the regular flutter run serving the page on port 3001, and then having a proxy service running on port 3000 that forwards all requests to port 3001 with the right headers added to the responses. Dart uses a websocket for debugging, but I can simply redirect that to the original port, since I don't have to change any headers for that one and websockets don't have CORS issues.

To Reproduce

I created a simple proxy in Rust for serving the Flutter code with the right HTTP headers:

use std::str::FromStr;

use warp::{
    http::HeaderValue,
    hyper::{
        header::{
            HeaderName, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
            ACCESS_CONTROL_ALLOW_ORIGIN,
        },
        Body, Response,
    },
    Filter, Rejection, Reply,
};
use warp_reverse_proxy::reverse_proxy_filter;

async fn add_headers(mut response: Response<Body>) -> Result<impl Reply, Rejection> {
    let headers = response.headers_mut();
    headers.insert(
        ACCESS_CONTROL_ALLOW_ORIGIN,
        HeaderValue::from_str("http://localhost:3000").unwrap(),
    );
    headers.insert(
        ACCESS_CONTROL_ALLOW_CREDENTIALS,
        HeaderValue::from_str("true").unwrap(),
    );
    headers.insert(
        ACCESS_CONTROL_ALLOW_HEADERS,
        HeaderValue::from_str("content-type, accept, authorization").unwrap(),
    );
    headers.insert(
        HeaderName::from_str("cross-origin-opener-policy").unwrap(),
        HeaderValue::from_str("same-origin").unwrap(),
    );
    headers.insert(
        HeaderName::from_str("cross-origin-embedder-policy").unwrap(),
        HeaderValue::from_str("require-corp").unwrap(),
    );

    Ok(response)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    warp::serve(
        reverse_proxy_filter("".to_owned(), "http://localhost:3001".to_owned())
            .and_then(add_headers),
    )
    .run(([127, 0, 0, 1], 3000))
    .await;

    Ok(())
}

Then in index.html, I have to change the websocket URL and load canvaskit from a local path:

    Object.defineProperty(window, "$dwdsDevHandlerPath", {
      value: 'ws://localhost:3001/$dwdsSseHandler',
      writable: false
    });
    window.addEventListener('load', function (ev) {
      _flutter.loader.loadEntrypoint({
        serviceWorker: {
          serviceWorkerVersion: serviceWorkerVersion,
        },
        onEntrypointLoaded: async function (engineInitializer) {
          const appRunner = await engineInitializer.initializeEngine({
            canvasKitBaseUrl: '/canvaskit/',
          });
          appRunner.runApp();
        }
      });
    });

Then I have to update the launch configuration of vsc:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "myapp",
            "request": "launch",
            "type": "dart",
            "args": [
                "--web-port",
                "3001",
                "--web-launch-url",
                "http://localhost:3000"
            ]
        }
    ]
}

My loader code:

import 'dart:async';
import 'dart:html';
import 'dart:js_util';

// all generated imports here

const root = 'pkg/myapp';

@JS('wasm_bindgen')
external Object Function(String) get _noModules;

FutureOr<Native> load() async {
  if (crossOriginIsolated != true) {
    return Future.error(const MissingHeaderException());
  }

  eval('window.wasm_bindgen = wasm_bindgen');
  final module = _noModules('${root}_bg.wasm');

  final module2 = await promiseToFuture<NativeWasmModule>(module);
  try {
    return NativeImpl(module2 as ExternalLibrary);
  } catch (e) {
    print('Wasm failed: $e');
    rethrow;
  }
}

(Note that I had to implement all of that myself instead of using Modules.noModules in order to implement error checking, because the dart2js compiler produces JavaScript code that doesn't propagate exceptions in async contexts.)

This is the error I get (from the catch above):

Wasm failed: Expected a value of type 'FutureOr<WasmModule>', but got one of type 'LegacyJavaScriptObject'

My research indicates that this might be caused by the @JS and @anonymous annotations of the NativeWasmModule class not being from package:js, but being homebrewn inside flutter_rust_bridge. The official annotations with the same name are used by the Dart compiler to do typecasts differently (according to the documentation, I haven't found the code for that yet), so this might be the reason why it can't cast a JsObject (which is the superclass of LegacyJavaScriptObject) to an anonymous class, which should work fine, according to the Dart documentation. (EDIT: using package:js's annotations doesn't fix it.)

I suspect that non-debug profiles simply skip the type checking and that's the reason why it works there. Again, I haven't found the code in the Dart compiler for that yet, though.

Expected behavior

Everything runs fine without exceptions.

Version of flutter_rust_bridge_codegen

1.61.1 (with my PRs applied)

Flutter info

[✓] Flutter (Channel stable, 3.7.0, on Microsoft Windows [Version 10.0.19044.2364], locale en-AT)
    • Flutter version 3.7.0 on channel stable at D:\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision b06b8b2710 (4 days ago), 2023-01-23 16:55:55 -0800
    • Engine revision b24591ed32
    • Dart version 2.19.0
    • DevTools version 2.20.1

[✓] Windows Version (Installed version of Windows is version 10 or higher)

[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.dev/docs/get-started/install/windows#android-setup for detailed instructions).
      If the Android SDK has been installed to a custom location, please use
      `flutter config --android-sdk` to update to that location.

[✓] Chrome - develop for the web
    • Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

[✓] Visual Studio - develop for Windows (Visual Studio Community 2019 16.11.18)
    • Visual Studio at C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
    • Visual Studio Community 2019 version 16.11.32802.440
    • Windows 10 SDK version 10.0.19041.0

[!] Android Studio (not installed)
    • Android Studio not found; download from https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/docs/get-started/install/windows#android-setup for detailed instructions).

[✓] VS Code (version 1.74.3)
    • VS Code at C:\Users\micro\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.58.0

[✓] Connected device (3 available)
    • Windows (desktop) • windows • windows-x64    • Microsoft Windows [Version 10.0.19044.2364]
    • Chrome (web)      • chrome  • web-javascript • Google Chrome 109.0.5414.75
    • Edge (web)        • edge    • web-javascript • Microsoft Edge 109.0.1518.61

[✓] HTTP Host Availability
    • All required HTTP hosts are available

! Doctor found issues in 2 categories.
fzyzcjy commented 1 year ago

/cc @Desdaemon who implemented the great web part :)

anlumo commented 1 year ago

So, the main issue is that the Dart code is casting types around all the time (from subclass to superclass, back to subclass, etc), and the js interop can't cope with that.

In release mode with -O3, the type checks are simply omitted. I haven't found a way yet to remove them in debug mode. Also, this seems to be a bad idea to do globally anyways.

Right now I'm trying to get the generated code to not do any casting in the first place, but as this is the fourth day I'm spending on this, I'm running out of time and might have to scratch flutter_rust_bridge completely for the project.

fzyzcjy commented 1 year ago

This means that there's no debugging and no hot reload on that target, which makes it pretty much impossible to develop.

Btw flutter itself does not support hot reload (though support hot restart), on Web

anlumo commented 1 year ago

Yeah, I meant hot restart. The debugging is the bigger issue anyways.

anlumo commented 1 year ago

So, an update:

I've finally managed to fix the issue in my fork. It's mostly adjusting the data flow to use generics instead of casting the type of the wasm_bindgen LegacyJavaScriptObject (which doesn't work).

I've also managed to fix the simplified initialization shown in the documentation here, which is way more complicated than getting it to run in the first place, because there's a weird loading system that's designed for ES modules that don't work with web workers anyways.

Now, I don't know yet how to feed this back into this project, since I don't have any idea how to do tests for this, since it's basically compiler configuration. I'll deal with that later, I need to rebase all of my changes to the latest version first anyways.

Also, there's the question of the development server. Mine is very different than the serve.dart shipped with this project, because I'm doing a reverse proxy server that just relays the flutter run server (and so hot restart and the Flutter inspector are fully supported). However, it needs a different setup, specifically two extra flags to the flutter run command (as documented in the ticket description here). So, this would need new documentation.

Also, this dev server does not run the wasm-pack command, because that's outside of the scope in this architecture (would not be enough to run this on startup when you can change the Rust code during development). This would need some other build system setup like cargo-make, preferably using some kind of file change watcher.

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

anlumo commented 1 year ago

👀

Andrflor commented 1 year ago

What about adding coi to web/index.html in the \ section ? https://github.com/gzuidhof/coi-serviceworker

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] commented 1 year ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new issue.