tauri-apps / wry

Cross-platform WebView library in Rust for Tauri.
Apache License 2.0
3.38k stars 252 forks source link

feat: synchronous communication between webview and rust #454

Open amrbashir opened 2 years ago

amrbashir commented 2 years ago

Tauri has a lot of functions in its Js API that can and should be synchronous instead of async, for example the homeDir function in tauri's path module. Synchronous communication can also help boost the performance since it doesn't include any serializations (I think).

Would you assign yourself to implement this feature?

wusyong commented 2 years ago
* Webkit2gtk not sure if it has something similar or not but here is a good [article](https://blogs.igalia.com/carlosgc/2013/09/10/webkit2gtk-web-process-extensions/), maybe?

The article is outdated and, according to webkitgtk dev, they are deprecating web extension. So sad news is we will rely on custom URL scheme unless someone can find a better solution for this. The good news is webkit2gtk 0.36 will come with headers support!

nklayman commented 2 years ago

I think the only way we could get this to work on linux without using webextensions would be to make a synchronous XHR request to a custom protocol handler. It'd be pretty ugly implementation-wise, and I honestly don't think this is a necessary feature. We could make functions like homeDir synchronous by sending the data during load and caching it in JS for synchronous access. Are there any other functions that need to be asynchronous?

sagudev commented 2 years ago

The article is outdated

Here is one newer example, but it on sketchy Chinese site: https://chowdera.com/2021/04/20210404232806125d.html.

according to webkitgtk dev, they are deprecating web extension

Where is stated that they are deprecating web extension?

amrbashir commented 2 years ago

I think the only way we could get this to work on linux without using webextensions would be to make a synchronous XHR request to a custom protocol handler. It'd be pretty ugly implementation-wise

I say we do it on linux only, even if the implementation will be a bit ugly. We are only gonna need it for a small subset of functions but it will improve the ergonomics of using tauri JS API.

We could make functions like homeDir synchronous by sending the data during load and caching it in JS for synchronous access.

That could work but won't work for all cases (see below) and we will have to cache it for each window and it could slow the loading of web content.

Are there any other functions that need to be asynchronous?

There is other functions like join, normalize, resolve and maybe some sync versions of fs module functions.

Also worth noting that the webview2 and WkWebView APIs that I linked above should have higher performance than we our current implementation whether we used it synchronously or asynchonously.

nklayman commented 2 years ago

I don't think that any fs functions need to be synchronous. join, normalize, and resolve could all be implemented in JS with cached data I believe. If it has to access the filesystem for the call then the call shouldn't be synchronous anyways as it will block the UI. With async/await it's so easy to handle async functions nowadays.

amrbashir commented 2 years ago

I don't think that any fs functions need to be synchronous. 

Users have asked for sync versions of fs functions a couple of times. It is not optimal but they still ask for it.

joinnormalize, and resolve could all be implemented in JS with cached data I believe.

It will be an ugly implementation IMO and won't allow sharing the same logic in the rust API.

If it has to access the filesystem for the call then the call shouldn't be synchronous anyways as it will block the UI.

It will block the UI but that's the user choice not ours.

With async/await it's so easy to handle async functions nowadays.

I would have to partially disagree here. Think of

const config = await readTextFile(await join(await homeDir(), ".config/conf.conf"));

vs

const config = await readTextFile(join(homeDir(), ".config/conf.conf"));
nklayman commented 2 years ago

Users have asked for sync versions of fs functions a couple of times. It is not optimal but they still ask for it.

People ask for lots of things. If it were easy, I'd agree that we should do it but in my opinion it's definitely not worth it just because some people asked for it. It's a bad idea that would require an ugly implementation.

It will be an ugly implementation IMO and won't allow sharing the same logic in the rust API.

Yeah it wouldn't be the prettiest but it would be way better than sync XHRs to a custom protocol, plus it would just be one implementation for all platforms.

It will block the UI but that's the user choice not ours.

Again, I personally don't think we should be putting all this effort and complexity into what would be a bad API.

As for the example you provided, I do agree that the second one is much nicer. However, that could be achieved with the caching solution (I think). I think we should have a vote with the core team and other wry contributors on the best way to approach it, because adding synchronous communication support is a significant change.

wusyong commented 2 years ago

Where is stated that they are deprecating web extension?

@sagudev Here's the comment on their matrix.

adminy commented 2 years ago

Users have asked for sync versions of fs functions a couple of times. It is not optimal but they still ask for it.

People ask for lots of things. If it were easy, I'd agree that we should do it but in my opinion it's definitely not worth it just because some people asked for it. It's a bad idea that would require an ugly implementation.

Then you should give people a js backend. Because some libs just can't work without sync.

It will be an ugly implementation IMO and won't allow sharing the same logic in the rust API.

Yeah it wouldn't be the prettiest but it would be way better than sync XHRs to a custom protocol, plus it would just be one implementation for all platforms.

It will block the UI but that's the user choice not ours.

Again, I personally don't think we should be putting all this effort and complexity into what would be a bad API.

As for the example you provided, I do agree that the second one is much nicer. However, that could be achieved with the caching solution (I think). I think we should have a vote with the core team and other wry contributors on the best way to approach it, because adding synchronous communication support is a significant change.

Can't be comparing to electron if it hasn't got such an essential feature. I agree that it should be simple solution, but not non-existent.

nothingismagick commented 2 years ago

Well, we have never said that Tauri will solve everyone's problems all the time. And "essential" features are always in the eyes of the beholder. That said, you are probably right. We should stop comparing ourselves to Electron, because while there is a good deal of overlap with that ecosystem, we are seeking to help folks discover new and better ways of doing things.

Since you are new to this specific discussion, perhaps you would like to share some CONCRETE examples of libraries that "just can't work without sync", and use cases that you see preventing you from using tauri. This helps us understand your perspective better.

adminy commented 2 years ago

@nothingismagick I'm not sure that I understand how that makes it better way of doing things, since you're introducing functional impurities for the user code. But I do agree, that it's probably only "essential" from the point of view of supporting libraries, and maybe just very useful to have so that not all my code becomes a promise of some kind.

I'm talking about npm packages of course, my use-case was around finding java home and getting it to work as is with no changes but any library library that uses child_process or fs module. You can trick the require into giving some builtin polyfills which can fix a ton of npm packages.

That said, good decoupling of os to non-os code can solve the problem I'm having. But then its much easier to prototype in electron and only truly think of tauri in a critical production app which requires a lot more hours of engineering to put in, most likely whatever is easier to use will be the default if performance also comes at ease of use cost.

JonasKruckenberg commented 2 years ago

I like the prospect of having synchronous (and serialization free) API functions. However, this is about prioritizing and I think it would be a waste of our (rather limited) time, especially when the API package has far bigger usability problems then sync vs. async.

I'm also worried about the current trend, that many users implement all logic (including file processing) in the frontend. We should stress at every possible occasion that this is a really, really, really bad idea instead of encouraging it through synchronous commands (any many other features that have been heavily requested). The risk we're running is that any people end up with very slow, incredibly insecure apps because they literally enable all api functions and use them liberally.

We should instead emphasize that tauri works like a normal server/frontend setup and the api package is basically fetch and not electrons nodeIntegration. Every frontend developer understands that synchronous fetch can't exist and would be a very bad idea.

So if anything, this is an argument for prioritizing the deno integration after v1, to give all those users that are deathly afraid of even looking at rust a better option.

nothingismagick commented 2 years ago

The risk we're running is that any people end up with very slow, incredibly insecure apps because they literally enable all api functions and use them liberally.

I think this has a lot to do with the perceived heavy lift of using rust at all, and the fact that small teams / single-person teams are trying to do everything anyway they can.

nothingismagick commented 2 years ago

But then its much easier to prototype in electron - see @adminy this is the thing. This is a node-centric perspective, but I would venture a guess that many people reading this would actually argue that its easier with rust. You can absolutely write the same functionality in Rust (with a bit more safety probably).

We are not trying to replace Electron / nodejs with something better because its in Rust :tm:, but aiming to help application engineers solve their real problems in a safe and performant way. Monkeypatching the entire nodejs ecosystem is NOT something we are ever going to solve, not even with upcoming fetch or https imports in node.

My point here is that the way things have evolved in the JS ecosystem just aren't always appropriate for the Tauri ecosystem. Like, really - why use a webworker or wasm when you should be using the rust backend? I am not raging on JS or the Webview, because to be fair websockets in rust can be slow and who doesn't enjoy the fun of parsing floats in JS.

But in the context of your example:

const linux = runExec => { // return the result of a child process
  return runExec('update-java-alternatives', ['-l']).stdout.toString() // specifically run this cmd
    .trim() // cleanup
    .split('\n') // make an array
    .map(line => line.split(' ')) // make another one
    .map(([version, vendor, home]) => ({ home })) // create your final array
 }

This is great stuff to learn how to do in rust, and I would encourage you to investigate how you would do it with rust and see this as a learning opportunity...

See this page for insight into how Tauri can help you: https://docs.rs/tauri/1.0.0-beta.8/tauri/api/process/index.html

(but note with 1.0.0 there will be a slightly different syntax)

And if you haven't gotten around to it, then I recommend diving headfirst into the rustlings course.

[edit] And on a total tangent: http://rosettacode.org/wiki/File_modification_time https://rosettacode.org/wiki/Category:Rust (for insight into how to do EVERYTHING)

JonasKruckenberg commented 2 years ago

Monkeypatching the entire nodejs ecosystem is NOT something we are ever going to solve, not even with upcoming fetch or https imports in node.

We can't blame any developer for this though. The api heavily suggests that it has capabilities comparable to nodejs. I definitely see potential for improvement here, be it documentation or revising some functions. Serialization free functions would help immensely but don't deliver enough value right now IMO.

adminy commented 2 years ago

A lot of the time the developers that do the UI usually do the backend logic since its a single app. Having that done in 2 different languages is not great. Either UI in Rust (makepad.dev) project. Or the entire backend in js. Security shouldn't be a concern sometimes. Especially if you're loading a local index.html file and just parsing it. just block the console access in releases.

nothingismagick commented 2 years ago

Security shouldn't be a concern sometimes.

I would like to remind you that security is always a concern, it will always be a concern, and ignorance of the concern will not save you. Console access is the least of your worries, especially if your index.html uses js that comes from any node.js / npm ecosystem. A lot of the time developers usually don't worry about things because they just want to get sh*t done. We are here to change the myth of free as in rides.

Your point is noted, but I don't think we need to continue this discussion right now under the premises that security might not be important.

wusyong commented 1 year ago

I updated top comment a bit to reflect more up-to-date approaches we can choose. All platforms have ways to do it natively without too much overhead now. However, their APIs are still very different. I've been thinking about this for days, but still can't come up with a decent solution. And I'm not fond of CreateSharedBuffer on Windows personally. Its usage could be error-prone. It could cost large memory leak if we don't handle it well.

wusyong commented 1 year ago

I updated the oc comment again to reflect what possible approaches we can choose again. So the good new is every platform can do so. But the bad news is their interface is very different, leading to what kind of type we want to pass. They all seem to copy the data, so I feel like this won't increase inefficiency than current custom protocol too much. I'll write down each platform's problem and what we should research more.

Windows

AddHostObject should be used to create unified interface to Mac and Linux. I still not the fan of shared buffer. To do this we have to define a COM object which implement IDispatch. And since we can't use type lib and IDL to auto generate the bindings. We'll implement it manually. This means we must handle invoke and pass it to the right functions.

macOS

This becomes the most constrained platform. The native types it supports is limited. Many JS values just become NSDictionary, even Typed Array. This will cause performance problem IMHO. We will have to define some minimum set of types that can share with other platforms.

Linux

I updated javascriptcore-rs to support more JSValue construction. These include ArrayBuffer and TypedArray. I would say this should be the easiest platform to do so.

So for our next step, we will have to define some types that every platform can use them well, especially on macOS.#929 cc @lucasfernog @JonasKruckenberg This is what I got so far. It would probably take another long time for us to define parameter types that everybody is happy. And since they also have to copy data, it's okay to use custom protocol in the near future IMHO.

defims commented 3 months ago

For webview2, both createSharedBuffer and AddHostObject are well supported. For example, in wvwasi, I implemented the registration of synchronous interfaces based on wry's controller. For ease of use, I also developed wvwasi_macro::create_type_info_crate to simplify the use of AddHostObject.

Mudloop commented 1 month ago

It is possible to do synchronous http requests in the browser, not with fetch but with XMLHttpRequest (or sync-fetch which makes it nicer).

just sharing because it might be an easy route to do synchronous api calls, or even if it’s not something that would be officially supported, it might be useful information for someone who is looking to do some synchronous calls themselves.

JonasKruckenberg commented 1 month ago

I think it is safe to say we will not officially support sync IPC unless proper shared memory primitives are well supported across enough target platforms. Given the current circumstances async IPC is the "pit of success" for most use cases and giving people a possible footgun to play with only makes sense when the benefits of the footgun are big enough, i.e. full proper zero-copy IPC through shared memory and atomics.

Last time I spoke with both apple and igalia engineers they were not opposed to the idea of adding shared memory support to both versions of webkit though, so we might actually see this issue close at some point.

Until then though, consider this issue on indefinite hold.