Cross-platform web UI for C and Go

Cross-platform GUI has always been a painful part of software development.

Today the biggest horror for an old-school C developer is to look at the list of Electron apps on their website. Simple, often trivial utilities, each taking over a hundred of megabytes of disk space and consuming hundreds of RAM once launched.

I’m not going to start a rant here, people of HackerNews have said enough on this topic. Electron wins because it is easy to use and everyone knows how to write HTML/CSS (unlike, say, QML or XAML).

Still, there must be a better way. Even though RAM and disk space are cheap, it just doesn’t feel right.

I obviously won’t be the one to fix it with a silver bullet, but at least I can try. My today’s Sunday morning exercise was to make the smallest possible wrapper for various WebView implementations on Linux, MacOS and Windows and turn it into a cross-platform native GUI library that uses HTML/CSS for rendering.

Webviews

Let’s start with macOS. It comes bundled with a nice WebKit library, and that WebView can be easily integrated into a Cocoa app. No troubles here. Linux is a bit on a darker side. From what I’m aware of, modern Debian-based distributions don’t come with WebKit library preinstalled. However, due to the simplicity of package management and the smartness of an average Linux user, it should not be a problem to install gtk-webkit manually.

Windows, in contrast, is a total disaster. There is MSHTML and there is EdgeHTML. The first one is our dearest Internet Explorer, and the latter doesn’t have any APIs to embed it from the C code.

After spending some time trying to learn about UWP and EdgeHTML, I ended up embedding MSHTML via OLE, like in the old days. Surprisingly, it gives rather satisfactory results.

On Windows 8 user will end up with IE10, which is still broken, but at least supports most of the CSS3/HTML5. On Windows 10 it will be IE11, which is even better. But poor Windows 7 users will be enjoying IE7 in all its glory and there is not much we can do about it. And I don’t want to even start talking about Windows XP…

API

Of course it would be nice to have an advanced API with a flexible event loop and functions to control every aspect of a web view and the hosting window, but that’s beyond the time limits of a Sunday morning.

The library is available at https://github.com/zserge/webview

So I simplified the API down to one function call:

int webview(const char *title, const char *url,
	int width, int height, int resizable);

It is supposed to open a window with the given title, width and height (optinally - resizable) and render a full size web page with the given URL.

There is no way to provide actual HTML contents, but on Mac and Linux you can pass data:text/html,<html>....</html> instead of a URL.

Usage

The whole library is just a single header file of ~700 LOC and is available on github. It’s written in C99, so it should play nicely with other languages if you want to make bindings. I expect it to be used only for web UI rendering. I don’t force you to use any specific JS frameworks or communication methods with the main application core.

I would probably use a WebSocket for communication with the “backend” part of the app. If you can afford sharing you code under a GPL license - you can use Mongoose web framework, it supports WebSockets.

But in 2017 I would also consider using Go.

From C to Go

Of course, there is little fun in writing web apps in C. So I made a tiny cgo wrapper that allows opening a webview from Go.

A self-contained native app with webUI can look like this. It shows a greeting with the current user name and exits if you click the button. You can check the screenshots on the project github page.

package main

import (
	"html/template"
	"log"
	"net"
	"net/http"
	"os"
	"os/user"

	"github.com/zserge/webview"
)

var tmpl = template.Must(template.New("").Parse(`
<html>
	<head>
		<style>
		* { margin: 0; padding: 0; box-sizing: border-box; font-family: Helvetica, Arial, sans-serif; }
		body { color: #ffffff; background-color: #03a9f4; text-decoration: uppercase; font-size: 24px; }
		h1 { text-align: center; font-weight: normal}
		form { margin-left: auto; margin-right: auto; margin-top: 50px; width: 300px; }
		input[type="submit"] {
				border: 0 none;
				cursor: pointer;
				margin-top: 1em;
				background-color: #ffffff;
				color: #03a9f4;
				width: 100%;
				height: 2em;
				font-size: 24px;
				text-transform: uppercase;
		}
		</style>
	</head>
	<body>
	  <form action="/exit">
			<h1>Hello, {{ markdownhack }}!</h1>
			<input type="submit" value="Exit" />
		</form>
	</body>
</html>
`))

func main() {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()

	go func() {
		http.HandleFunc("/exit", func(w http.ResponseWriter, r *http.Request) {
			os.Exit(0)
		})
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			u, _ := user.Current()
			tmpl.Execute(w, u)
		})
		log.Fatal(http.Serve(ln, nil))
	}()

	webview.Open("Hello", "http://"+ln.Addr().String(), 400, 300, false)
}

The good thing is that unlike many Electron apps, this demo consumes only ~6MB of RAM and the binary size is about 7MB unstripped (I’ve only measured it on Linux).

Of course, it’s a very early proof-of-concept and I haven’t used it in any serious apps yet. But the whole idea of running a native web UI on top of a memory safe modern language is very appealing to me. So if you share my views, or found a bug, or have a suggestion to make - feel free to contribute!

I hope you’ve enjoyed this article. You can follow – and contribute to – on Github, Mastodon, Twitter or subscribe via rss.

Aug 20, 2017