Making a Statically-Linked, Single-File Web App with React and Rust
anderspitman.net
anderspitman.net
An option in the middle is webview[0] which will "appify" it by using OS-native browser lib so you don't have to carry Chromium, V8, or all the multi-process ugliness but you still get the web stack.
Also everybody, don't forget to check your Host headers when building apps running HTTP daemons locally or you'll be open to DNS rebinding attack. And if your use case can support it, a random port is nice too.
0 - https://github.com/zserge/webview 1 - https://en.wikipedia.org/wiki/DNS_rebinding
What? This is nothing like Electron. What it's like is shipping the `node` executable to the end-user along with a .js script. The whole point of Electron is that it bundles a browser.
The parent's point is not that it's like Electron in that it bundles a browser, but that it's like Electron in that it allows you to bundle a web application into an executable.
The difference is that it doesn't include the rendering engine along.
To sum up my rant, I'm afraid the fast moving web stack, coupled with the fact only a few companies can keep up, coupled with the fact that those companies are laser focused on their own browser use case and not embeddability combine to make this incredibly common desktop use case still suck. In the meantime, just hope these OS engines stay minimal and don't fall too far behind the standards you want.
Packaging my own browser with my code so that I am now developing toward ONE client instead of any random thing that can speak http, and at the same time, being able to take the core of my app, tweak it a little, and plop it out on the web without a complete rewrite ... that is completely amazing. It's what I've wanted since forever.
Even better would be the ability to optionally include features in my build. Like if I don't NEED WebGL, the MIDI and Audio APIs, etc, etc ... it'd be pretty nice to be able to optionally exclude those from the bundled browser to minimize size.
However, I don't want to be welded to google. Mozilla's codebase seems ripe for such a development.
Tried, but abandoned presumably based on prioritization: https://github.com/mozilla/positron. I was hoping Servo would help here, but from the outside looking in, the pieces are being moved into FF proper and the Servo browser's priorities have been reshifted to being a part of the VR team. Sure they're still making a general use browser (and that's quite a feat), but the embeddability game may suffer (I believe conforming to the CEF iface that was there original goal has become stale).
(I'm not condoning this, and haven't worked with Electron myself, but that's my understanding of the motivation.)
Like everything in fashion, next season trends will be other ones.
Ah, that's a good explanation as to why zserge/webview recommends serving using ephemeral ports.
Also, wanted to pull in from the readme: webview supports interacting with the javascript environment directly, so a web server isn't strictly required.
Why should a desktop app even care about host headers?
Because the packaging and distribution experience sucks. Seriously, that's all it is. Make it so that I can run one command and turn my GTK app (written in a language that has a decent package manager and library ecosystem i.e. not C or Vala) into single-file executables for all major platforms, and you'll take the marketshare back from Electron.
I'll agree that packaging desktop apps is still annoying, but traditionally the packaging experience is handled elsewhere in your stack. I'm not sure that expecting a widget API to provide lifecycle management is the wisest choice...
Electron is a full stack, yes, but GTK slots into any number of stacks. Not my area but putting together a build script that outputs different formats (for an app that just requires some file copying for install) can't be that bad. Python in particular has lots of options for cross-platform packaging and distribution. (py2exe, etc)
From a developer's point of view, Electron solves that problem. It's not about what the underlying library is, it's about what the whole package does.
> Not my area but putting together a build script that outputs different formats (for an app that just requires some file copying for install) can't be that bad. Python in particular has lots of options for cross-platform packaging and distribution. (py2exe, etc)
All the pieces are there, but no-one's put them together in a nice, well-supported way. Packaging a python application like that is a bunch of tedious manual gruntwork that's easy for a beginner to get wrong.
If you're asking "why not link to gtk webkit?": because I don't want to statically compile or ship with the entire browser (not to mention Windows compat). If you're asking "why not build your app on gtk instead of web tech?": there are a million reasons and this question happens frequently, so not really sure it's worth rehashing here.
> Why should a desktop app even care about host headers?
It's one way to prevent DNS rebinding attacks. You can employ other methods to ensure the client is "authenticated" to use the server. OP's app listens on port 5000. I can have a page on example.com:5000 and set the DNS zone to a really low TTL change its A record to think it's 127.0.0.1 after first load thereby letting the browser think I'm same origin w/ just an IP change, then I can ajax call to example.com:5000 to access the local daemon. That's how DNS rebinding attacks work and host header checking is one way to for the local web server to prevent other pages from accessing it. Project Zero is finding lots of local HTTP servers that are reachable from web pages.
> there are a million reasons and this question happens frequently, so not really sure it's worth rehashing here.
then > Project Zero is finding lots of local HTTP servers that are reachable from web pages.
Amazing. All signs point to not building apps manifesting as local web servers and people continue to do so anyway.A millions reasons, huh? A million reasons against, more like it.
The fact that you have to care about DNS rebinding attacks on a desktop app, where presumably that app does not do anything special with the network or DNS, is all kinds of smelly to me.
I wish I could show off some of my work in this area but we took an old JS/HTML/CSS 1.0 stack and did our 2.0 in GTK (with all the fancy graphics and everything), and we are much happier now -- a ten-year-old single-core Celeron system with little RAM runs our stuff great (whereas before we had issues)
Glad I'm not the only one.
When I was younger, I made a hobby Linux distro that ran completely on floppies with a custom filesystem hierarchy. All the binaries were in /Programs and were statically linked against uClibc. Even X was statically linked, there was some abandoned branch of XFree86 that ran with a VESA driver and fit under 1.44 MB. I thought that I was the bee's knees.
All this was still possible ~2007. Not sure how masochistic you'd have to be to try today.
If you're interested in stuff like this for go, I've used https://github.com/GeertJohan/go.rice with great success.
[EDIT]
"feature" -> "side effect"
"rust and golang" -> "rust and golang being able to produce static binaries"
For example, you can do the import with gcc this way: https://balau82.wordpress.com/2012/02/19/linking-a-binary-bl...
With Green Hills, there is a .rawimport directive.
Apple and Microsoft systems have been supporting this on their native toolchains since the mid-90's.
I think what is new is that in the mid 90s you didn't have an open source, memory safe, ergonomic, strongly typed, type inferenced language that targets multiple platforms with ease, with a decent standard lib. Rust is delivering that. Golang is delivering that. I think those two are novel in that sense.
From where I'm sitting the last few decades have been ruled by VMs and the explosion of scripting languages, because no one wanted to use those Apple/Microsoft toolchains that have been around in the 90s. While those toolchains rested on their laurels (or didn't, can't tell the difference now), Java became the enterprise workhorse.
Many devs do actually enjoy those Apple/Microsoft toolchains.
Java was marketed pretty closely to that.
Make you can use qmake as a prebuild step and include the generated moc files in your build? I wonder how many qt headers are in the generated source files.
[EDIT] - I didn't address your question about go.rice -- from what I know, it doesn't have this kind of thing built in, and I don't know that it even should... "flipping a production switch" is a pretty application-specific endeavor. Also this sounds like something you should be doing with your build tools..., can't include something in the binary at runtime. Maybe I misunderstood what you were asking.
To clarify, the description for go.rice is:
> go.rice is a Go package that makes working with resources such as html,js,css,images,templates, etc very easy. During development go.rice will load required files directly from disk. Upon deployment it is easy to add all resource files to a executable using the rice tool, without changing the source code for your package. go.rice provides several methods to add resources to a binary.
My question is really geared towards this pain point: the React/JS stuff can be quickly rebuilt for almost instant feedback. But even for a simple app like this example the Rust compilation takes a couple seconds on my system. You have to pay that cost even if you're just updating the JS because it has to be built into the binary. It would be nice if you're not changing the actual Rust code to be able to easily reload your JS without compiling.
Oh I think you absolutely can have that kind of flow -- go.rice DOES support that. Also, I still don't really understand because this problem seems to be easily solved by just changing your build script, or detecting environment at runtime. You could even check for the data, and if it's not present, fall back to disk.
I don't know a library that does it off the top of my head for rust though, since I'm not that familiar with rust dev.
I'm thinking something a little fancier than include_str! because I want to include a whole subdir of resources, and I probably want to embed the gzipped version (and uncompress into RAM for when there's no "Accept-Encoding: gzip") rather than the reverse.
Build an interface for the go.rice (or go-binddata/etc) such that, depending on the environment, it either pulls the file from disk or from memory. Super easy.
I seem to recall seeing a Go library that actually has this functionality built in, but I've got no idea which it was.
But yes, it is just like including files in your classpath (and making sure they end up in the jar), then serving them from a Netty endpoint or something.
It is only a matter of buying one of the commercial JDKs, or if doing GPL stuff use the open source license from ExcelsiorJET.
Those not willing to pay and already on Java 9/10, running on Linux x64, can play with the initial AOT support.
A JAR is meant to run on the JVM. There is no such thing as a "statically linked jar". The process you're referring to is more like compiling bytecode to assembly, you're converting a JAR, a thing meant to run on the JVM, to native machine code (with assembly as the step in between).
> it's only a matter of ...
Yeah, it sounds simple in theory, but somehow I don't find many projects these days that use these commercial JDKs to generate VM free assembly. In fact, I don't even know one big tech company that does so -- maybe you could enlighten me.
> Those not willing to pay and already on Java 9/10, running on Linux x64, can play with the initial AOT support.
So just about the newest version of Java just got initial support? Cool. Well this brand new community-led thoroughly open-source language has this as a core tenet, and supports compiling to many platforms very easily.
Kudos to Java for getting better, embracing a more open development methodology (I think this has been the case since 8), listening to the community more about which features to include, but when compared to projects that took a fresh look at all these concepts, and started with the open community-based approach, Java doesn't compare (except in terms of speed, JVM is pretty heckin' fast).
Ah, and if you happen to own an Android phone running version 6.0 or newer.
The free beer version of Java never supported AOT because Sun was religiously against it.
The community never managed to gain enough mindshare to keep gcj going, which was used by Red-Hat to ship native compiled versions of Eclipse, Tomcat and JBoss.
Since Oracle thankfully has another opinion on AOT, they have a long term plan to bootstrap Java and remove C++ out of the equation.
Also, Android went from Dalvik to ART right? Those are both still VMs? Latest android looks like ART + JIT, but you can't AOT a JIT (that's the whole point of doing it Just In Time)?
Maybe I should take a look at Java 9/10, but if an employer isn't requiring it, and library support is somewhere near similar, I'm definitely considering doing the project in Rust first, then Clojure, then Frege, then Java 9/10.
Outside of the insane wealth of libraries that exist as a result of no one really having a good cross-platform choice for the last few decades, I don't think I'd choose plain Java for a project today. The JVM, maybe, but not plain Java on top of it. Luckily, I'm not the only developer out there, since there are tons of people who love java are still supporting it and using it, making cool things with it, and pushing it to be better.
Until version 5.0, there was only Dalvik, a register based VM with a very basic JIT compiler.
On version 4.4, ART was introduced, but you had to explicitly enable it and many OEMs did not had it available anyway.
ART was a pure AOT compiler, at installation time on-device.
Given that it was taken hours to recompile everything when updates came on phones with lots of apps, Android 7 introduced a multi-mode interpreter/JIT/AOT toolchain.
On 7 and later, when an application starts for the first time, the interpreter written in hand optimized Assembly is executed, then control is given to the JIT, which in turn makes use of PGO.
A background AOT compiler takes the PGO data generated by the JIT and creates an optimized AOT binary for the application.
The next time an application is executed, the AOT compiled binary is used, until there is some kind of change, like an update or unexpected execution path, that requires a recompilation to take place.
Dalvik was a JIT VM. Then, for Android 5.0 (Lollipop), Android moved to AOT compile code (On installation and ROM upgrade, which is why if you ever updated Android then, you'd be sitting and waiting for a few minutes while Android optimized apps).
Then recently (I don't remember if it was for Marshmallow or Nougat) Android went back to a JIT VM on install, followed by a background AOT compile. This way installation goes faster (it doesn't have to compile everything right away), ROM upgrades go way faster (you don't have to AOT compile tens to hundreds of apps before being able to use your phone), and Android can do guided optimization during AOT (it knows which hot-paths were taken).
http://openjdk.java.net/jeps/295
Basically Java AOT is half-assed solution like so many other Java solutions e.g Java GUIs, Java build tools, Java generics etc etc.
Either one replaces the engines in mid-flight, or parks the plane with perfect thought out solution.
Sound exactly like the kind of environments were all kinds of horrible crap are used. Doubly so with the mention of IBM.
I just mentioned those, because they have Java code in production, with soft-real time deadlines for weapon targeting systems of a few milliseconds.
Usually the kind of stuff that gets mentioned that GC enabled languages aren't capable of.
It is all a matter of budget and the army has lots of it, which incidently is what allowed boring stuff like the Internet to exist.
Sure, I don't have a beef with either (although someone could go non-JVM to get either AOT and/or real-time guarantees in a better form perhaps).
But I don't think "the X and Y army uses it" serves as proof that a technology is mature.
It's not like the army doesn't have all kinds of non-critical applications, and all kinds of legacy crap and modern crap apps lying around, doing some thing or another.
It's like saying "Facebook uses our app X" as proof of maturity, when the use could be in some horrible experimental project, that Facebook tried and semi-abandoned, or is the product of some engineers "20% time" stuff...
French radar system for ballistic missile tracking and measurement.
http://www.militaryaerospace.com/articles/2009/03/thales-cho...
Aegis Weapon naval defence system, deployed across US Navy cruisers
http://www.spacewar.com/reports/Lockheed_Martin_Selects_Aoni...
USS Bunker Hill ballistic missile defence system weapons control
http://www.militaryaerospace.com/articles/2010/04/aonix-perc...
Thanks for mentioning our product, but I have to make a few corrections:
First, the Excelsior JET Runtime license is sadly not GPL-compatible. Also, you cannot use it to target embedded systems, unless you buy Excelsior JET Embedded and pay royalties, though we are currently weighing the option to switch to OpenJDK to eliminate the latter.
Second, the Standard Edition is free even for commercial use (the above limitations apply).
Finally, other editions are available at no cost for use in public non-commercial projects. It does not matter whether the (software part of the) project is open source or not.
See https://www.excelsiorjet.com/free for details.
Thanks to this short blog post by OP I learned something that will let me simplify some of my code.
However the thing that I am doing is not completely useless, because I have been able to embed fonts, favicon and images as well with the method that I am using.
The code I have written for this is only the beginning but if anyone wants to look at my code in order to see how I did it here you go:
https://gist.github.com/ctsrc/4c4cc05254d12bbc8937a0ea385fcd...
It's far from great but it's a start. Let me know how you would do it better :)
Speaking of Rust and websites, in Python I have fallen in love with the pyTenjin templating engine. http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html. Does anyone know of a templating engine like pyTenjin but for Rust?
FWIW there's also an include_bytes! macro to embed binary data right into the executable without going through a separate rustification step.
My eventual plan that I remember now was to make it so that I could reference static files (CSS, JS, icons and images, fonts) in my static HTML files and HTML templates, and build.rs would automatically include the bytes of those files.
#[get("/fonts/<fname>")]
fn fonts<'a> (fname: String) -> Result<Content<Vec<u8>>, NotFound<String>>
{
match fname.as_ref()
{
"autobahn.woff" => Ok(Content(ContentType::WOFF,
include_bytes!("../static/fonts/Autobahn/autobahn.woff").to_vec())),
"autobahn.ttf" => Ok(Content(ContentType::TTF,
include_bytes!("../static/fonts/Autobahn/autobahn.ttf").to_vec())),
"grobe-deutschmeister.woff" => Ok(Content(ContentType::WOFF,
include_bytes!("../static/fonts/Grobe-Deutschmeister/grobe-deutschmeister.woff").to_vec())),
"grobe-deutschmeister.ttf" => Ok(Content(ContentType::TTF,
include_bytes!("../static/fonts/Grobe-Deutschmeister/grobe-deutschmeister.ttf").to_vec())),
_ => Err(NotFound(format!("No such file: /fonts/{}", fname)))
}
}
In the meantime since I first wrote my original code, the web framework I was using called Iron [1] has become unmaintained and they now urge people to pick a different framework. I chose to use Rocket because it seemed to have the features I want and the documentation seemed good enough to get started (and it was).For templating I found Askama [2], which I am using from git master in order to be able to use it together with Rocket. https://github.com/djc/askama/issues/71
Eventually I will generate the routes for the included bytes with build.rs again. My new code is better both short-term and for when I will create said build.rs.
Thank you masklinn for your comment about include_bytes! which made me look at my code again and rewrite it.
[1]: https://rocket.rs/
But, AFAIK, only supports Pure Perl code, so one is SOL for most database drivers etc...
It supports loading XS modules by overriding DynaLoader bootstrapping methods; it writes shared object file to a temporary file at the time it is needed.
0 - http://github.com/astraw/bui-backend 1 - https://github.com/DenisKolodin/yew
If the single binary that just serves static files you're better off with a web server that supports https, rules, vhosts, etc and is battle tested. Not to mention that this binary you created will need startup scripts of some sort depending on the platform.
I'm pretty sure the use case of this is more akin to an electron replacement, and not a production web server. So the user would just download and run the binary, and interact with the app from their browser.
Strange how some people are obsessed with hype. I think what raised interest of this article was just word "Rust" in the title. Rust seems to be wet dream of many developers. They heard it's safe, so it will magically solve all their problems. But the usage of Rust in this case is meaningless. You could do the same with python, nodejs, java, just plain nginx or million other (and better) ways. Just first google query returned probably better solution if you like single static binary http://miniweb.sourceforge.net/. It would be two files instead of one miniweb binary and static site compressed as 7z.
How does this thing scale? What about SSL? Do I have to put reverse SSL terminating proxy in front of it? Yes, you say? Ok, why not just use that proxy for serving those static files too (nginx does that) and skip this thing completely. What about performance? Have you tried to httperf on it?
I would appreciate if this did more then just being a single binary. Like Facebook's HipHop compiler of PHP to static executable or something like that. Sorry but this is real bullshit.
There's nothing else to see here, you either like dependency-less binaries or not.
Hell, if anything, I don't see the point of your point, calling static binaries "real bullshit". Depending on the scenario static binaries are either better for you, or worse for you - it's just a tool in a bag of tools. How can that possibly be bullshit? Is a wrench bullshit?
EDIT: I'm not against static binaries per se. Only against this usage of them.
Basically anywhere where reducing difficulty of deployment and asset management outweighs the valid concerns you pointed out. It's really, really handy in my view.
Of course, I've got no plans or desire to run some production documentation site on this. Yet, that problem domain is vastly different than internal tooling, OSS apps, etc.
Just think of a note web app you write. Do you want your users to have to manage css/template/image bundles just to use your note app? Why not just make the binary work with zero configuration/management? Plus, if you wanted - you can of course support both, allowing the user to override css files if they so desire, without having to recompile the binary/etc.
http://miniweb.sourceforge.net/. It would be two files. 20k server binary and 7z with site resources. It would safe me trouble of recompiling binary.
Know any good nginx books or courses?
I'm betting that almost every language that I've used has something like this, but the succinctness of it and how it's used to keep the source clean, but still compile in the text is really cool.
In theory this could be re-optimized with vmsplice, but I wonder whether that webserver actually does it because vmsplice's lifetime requirements are... complex (although a &'static str would meet those requirements). Or the binary could sendfile itself if it knew the position of the string in its binary image, but that information is not preserved by a &str.
This doesn't sound right to me - sendfile's "zero copy" is about eliminating the extra copy when you read() a file into a userspace buffer and then write() that file into a socket. If you have a memory-mapped file (which is what your executable itself is), and you pass an address inside that mapped region to write(), I would hope that the kernel will just access the buffers of that file directly - i.e., the fact that it's memory mapped means there is no separate copy of the file, there's a virtual page that references the single kernel buffer for that file. So at that point write() should behave just like sendfile().
If you need to process the file in the kernel itself, there will still be a copy, but you'd have that in either case. If you don't, I would again hope that both write() from a memory-mapped region and sendfile() know how to DMA the file from the disk to the network card.
I am not confident about this and it seems worth benchmarking. (Also, your production server probably uses HTTPS, at which point this is all irrelevant if your TLS encryption happens in userspace. If you're using in-kernel TLS, then you're definitely not doing DMA, and I would strongly hope that sendfile() and write() from a mapped buffer involve the same number of copies - one read from disk, at which point the kernel encrypts it into a writable buffer.)
Now if this was a production server having to service thousands of clients than the sendfile optimization becomes much more important.
Not if they are doing their own tls encryption which hopefully one day is the new normal.
The '!' means `include_str!` is a macro in Rust, similar to a C macro in function, but Rust has a much better system for macros and ships with many useful ones.
[0] - https://github.com/rust-lang/rust/blob/master/src/libcore/ma... [1] - https://github.com/rust-lang/rust/blob/e8af0f4c1f121263e55da...
In addition to the older hygenic macros (which have their own syntax to define them), Rust also supports proc_macros, which are actually code that is executed by the compiler and can preprocess a struct or function.
For more information, see the Rust Book (second edition) section on macros:
https://doc.rust-lang.org/book/second-edition/appendix-04-ma...
For static-linking the sqlite3 library, if you're using rusqlite you can just specify the bundled feature in Cargo.toml.
``` [dependencies.rusqlite] version = "your-version-number" features = ["bundled"] ```
It's not Rust, but it is a really crappy exercise tracking app I wrote for teaching a class.
Thing different about my code: I build the HTML in the source itself, and I don't use React
With a little bit of effort it's also possible to build and statically link native modules, eg I've had success with nodegit, uws and sqlite. (Needed to rebuild some of the node deps with musl, then rebuilding the native modules with musl, then modifying Node's node.gyp to --whole-archive include the additional .a/.o's (done in nexe), and using rollup to rewrite the .node includes to `process._linkedBinding('the registered module name')`, and then nexe builds the whole lot into a single executable.)
Nexe supports bundling resources (although less cleanly than `include_str` imo) so it can also bundle all your ui resources as well.
Its a bit short though. I would really like to see the setup of a simple uni- or even bi-directional rpc or value binding mechanism between react and rust.
Maybe json-api on a websocket on the same port as a transport would also be possible? Protobuf seems a bit overkill for something that is that tightly integrated together IMO. But if it works, it works.
There's just a small typo. The script path in the HTML mount file and the router path need to be the same, but do not match. Either one needs to change:
// ui/dist/index.html
<script src="/main.js"></script>
// src/main.rs
(GET) ["/bundle.js"] => {
Response::text(bundle)
},FROM scratch COPY ./bin/app /app ENTRYPOINT ["app"]
which will make a barebones image with no OS cruft. You need to make sure your binary _is_ truly statically linked though. The key being the inclusion of musl libc in in the OP's build.
I've tried this with golang and you need to pass some odd flags I can't remember off the top of my head to get it to not dynamically link gnu libc, but once working you have container images that are incredibly small.
I'd imagine with a minimal container host like CoreOS you get largely the same effect as running Erlang on Xen.
I'm wondering if people who prefer Phython prefer Rust? I've been developing for 20+ years and the syntax alone makes Rust feel prohibitive to me.
Still, I agree that Rust's syntax approaches C++ level of complexity at times. I find that writing actual statements and expressions is simple enough, but writing structs, lambdas, and function definitions requires knowing how to specify types, type bounds (if using generics), lifetimes (if using references), and Fully Qualified Syntax (for referencing types/items in other modules). And that's before actually having to come to terms with the borrow checker.
For what it's worth, once you get over the initial learning curve the cognitive burden goes down. Rust's syntax, to me, is actually visually distinctive enough that it's easy to parse out types, expressions, declarations, etc. from just a quick glance.
I highly doubt it's the syntax that's the issue. You're probably not use to caring about lifetimes and ownership. It's a different paradigm.
This comes up with Rust due to what's in the article:
> This part can be skipped if you don’t need 100% static linking. Rust statically links most libraries by default anyway, except for things like libc.
Rust statically links Rust code by default, but has a dynamic link to libc. So Rust binaries are "mostly statically linked" for this reason. MUSL lets you link that final dependency, getting up to 100%.
For instance, are these issues with rust you're having, or with a particular framework? Without any information, it's a meaningless question.