Embedding a Web View


#1

How hard would it be to make a library that opened a window/activity with just the OS’ own web view in it, mainly for the purpose of making very inefficient UIs? Does a library like this already exist?

struct WebWindow;

impl WebWindow {
    fn open(opts: Options) -> Self;
    fn eval(script: &str);
    fn close(self);
}

trait Handler {
    fn event(&self, win: WebWindow, dat: &str);
    fn closed(&self);
}

// Options:
// - Title
// - URL
// - Dimensions (+ Fullscreen?)
// - Handler
Platform Web View
Windows MSHTML / EdgeHTML (?)
macOS WebKit
Linux QtWebEngine / QtWebKit / WebKitGTK+
Android WebView
iOS UIWebVIew
Web Something Meta?

Note: I have no idea what’s going on with EdgeHTML.


#2

Does a library like this already exist?

zserge/webview

This one’s pretty much exactly what I want - it just needs more platforms and a Rust wrapper.

ChrisKnott/Eel

This one was coincidentally on the HN frontpage today!


#3

A few days ago I stumbled upon this thread, today I wrote some Rust bindings to this webview lib and translated the minimal and timer example, and they are working, yay :slight_smile:

The bindings code is still a bit messy but I will clean it up and put it on GitHub soon…


#4

Btw, this question needs to be answered before I can be sure which way to clean up the bindings:

(Currently I’m using a wrapper struct to make a *mut webview Send and not wrapping it in a mutex when passing it to the init callback, but it could be unsafe…)


#5

My bindings are almost ready, just have to fix this:


EDIT: This forum doesn’t let me add another reply, because I’m a new forum user and apparently new users are limited to 3 replies per topic…
Anyway…
I fixed the issue and I pushed my bindings to GitHub:



#6

@Boscop does this wrap the same library as https://github.com/alanhoff/rust-webview ?


#7

Yes, that’s the same library.


#8

Oh, I didn’t know about that one…


#9

Btw, I also published it to crates.io now: https://crates.io/crates/web-view


#10

FYI most GUI frameworks typically use thread local storage, so passing the *mut webview across threads (i.e. when you implement Send) may not be sound. They also don’t tend to be threadsafe, so implementing Sync is out of the picture too, you’d need a mutex if you want multiple threads to update the UI.


#11

I asked zserge and he said the C code already ensures that all dispatched callbacks are only called from the UI thread. So the mutual exclusion is done on the C side:

That should make my unsafe impl Send safe, right?

Or am I misunderstanding it?


#12

I just come across this web-view project. Is there a way to call native rust function from the js side? I was thinking if this could be used as a replacement to a rest API call from JS and instead call the equivalent native function in rust. This way, the back-end server don’t have to open a port.


#13

This is really interesting, I could definitely used this to have an easy binary installation for diwata database interface.


#14

Yep. If they’re doing synchronisation under the hood then you should be able to implement Send safely. It also means that any closures you receive for callbacks need to be Send as well.

I’m just wary because I’ve had issues with Gtk on Linux and the native Windows APIs where you can accidentally break things when updating the GUI from another thread.


#15

@Michael-F-Bryan Thanks, I added the Send constraint for dispatched closures.
I’m still not sure if all my lifetimes are correct, I would really appreciate it if you could tell me how to make the lifetimes better here: https://github.com/Boscop/web-view/blob/master/src/lib.rs

One thing that’s bad is that when calling the run() function, the ext_cb (of type F: FnMut(&mut WebView<'a, T>, &str, &mut T) + 'a) aka “external invoke callback” (which will be called from js as window.external.invoke() can’t be assigned to a var before calling run(), it has to be passed directly to run(), otherwise the compiler complains.
E.g. in the minimal example…

fn main() {
	let size = (800, 600);
	let resizable = true;
	let debug = true;
	let init_cb = |_| {};
	let userdata = ();
	run(
		"Minimal webview example",
		"https://en.m.wikipedia.org/wiki/Main_Page",
		Some(size),
		resizable,
		debug,
		init_cb,
		/* frontend_cb: */ |_, _, _| {},
		userdata
	);
}

If I try to change it so that the frontend_cb is assigned to a var before run() and then pass that var (like with the other args, for naming/documentation purposes), I get this error:

error[E0631]: type mismatch in closure arguments
  --> examples\minimal.rs:14:2
   |
12 |     let frontend_cb = |_, _, _| {};
   |                       --------- found signature of `fn(_, _, _) -> _`
13 |     let userdata = ();
14 |     run(
   |     ^^^ expected signature of `for<'r, 's, 't0> fn(&'r mut web_view::WebVie
w<'_, _>, &'s str, &'t0 mut _) -> _`
   |
   = note: required by `web_view::run`

error[E0271]: type mismatch resolving `for<'r, 's, 't0> <[closure@examples\minim
al.rs:12:20: 12:32] as std::ops::FnOnce<(&'r mut web_view::WebView<'_, _>, &'s s
tr, &'t0 mut _)>>::Output == ()`
  --> examples\minimal.rs:14:2
   |
14 |     run(
   |     ^^^ expected bound lifetime parameter, found concrete lifetime
   |
   = note: required by `web_view::run`

error: aborting due to 2 previous errors

How can I make this possible?


@ivanceras Yes, that’s exactly the use case that this lib is supposed to fit: Standalone Desktop applications with a web frontend. Your JS code calls Rust through window.external.invoke(), in the todo example you can see how it uses serde_json to auto deserialize a cmd sent from js into the Cmd enum:

let (tasks, _) = run("Rust Todo App", &url, Some(size), resizable, debug, init_cb, |webview, arg, tasks: &mut Vec<Task>| {
	use Cmd::*;
	match serde_json::from_str(arg).unwrap() {
		init => (),
		log { text } => println!("{}", text),
		addTask { name } => tasks.push(Task { name, done: false }),
		markTask { index, done } => tasks[index].done = done,
		clearDoneTasks => tasks.retain(|t| !t.done),
	}
	render(webview, tasks);
}, userdata);

// ...

#[derive(Deserialize)]
#[serde(tag = "cmd")]
pub enum Cmd {
	init,
	log { text: String },
	addTask { name: String },
	markTask { index: usize, done: bool },
	clearDoneTasks,
}

Here is the JS side of it.

For the other direction: From the backend you can eval arbitrary JS in the frontend.


Btw, you don’t have to do the backend/frontend communication via the two-way JS bindings. You can also do it by spawning a web server with hyper/iron/rocket/nickel or whatever, listening on an ephemeral port and exposing a normal REST API. Or even using server-sent events or websockets to push unrequested updates to the frontend. This is the easiest way to transition a previously server-based app to standalone, because you don’t have to change your frontend code.
Check out this part of the original webview readme.


Btw, the todo-purescript Rust example (the one whose screenshot is in the Readme) is 300kb (keep in mind that the rust compiler by default links statically to the rust runtime, unlike C++), and 153kb of that is the uncompressed included bundle.html (the app.js is inlined into bundle.html by the build script and then the rust code embeds bundle.html as a string (uncompressed) using include_str!()).


#16

Regarding the incorrectly inferred lifetime, you just need to write out the type of each closure argument.


#17

But closures can’t be explicitly polymorphic, right?

So how can I give the arg the explicit type &mut WebView<'a, T> when it should work for all 'a?

The callback passed to the run() function must have type:

F: FnMut(&mut WebView<'a, T>, &str, &mut T) + 'a)

where 'a is the lifetime of the webview:

pub fn run<'a, T: 'a,
	I: FnMut(MyUnique<WebView<'a, T>>),
	F: FnMut(&mut WebView<'a, T>, &str, &mut T) + 'a,
>(
	title: &str, url: &str, size: Option<(i32, i32)>, resizable: bool, debug: bool, mut init_cb: I, ext_cb: F, user_data: T
) -> (T, bool) {

It doesn’t work with let frontend_cb = |_: &mut WebView<_, T>, _, _| {};


#18

I think it should work with:

let frontend_cb = |_: &mut _, _: &_, _: &mut _| {};

#19

Ah yes, thanks, it works, but it’s not pretty :slight_smile:

And I’m not sure that all lifetimes constraints in this lib are correct.
I would appreciate if someone can suggest improved lifetimes…