Web のパワーでクライアントアプリケーションを構築 - WebUI


WebUI とは

Cで書かれたクライアントアプリケーション構築用のライブラリです(バックエンドとのブリッジ部分は TypeScriptで書かれています)。

フロントエンドには成熟したWeb技術を使い、バックエンドは C や Go, Rust や Python などを使って開発できます(WebUIの各種言語向けラッパーが提供されています)。

フロントエンドにはElectron 系のように WebView/BrowserView を組み込みで使うというものとは異なり、単にインストール済みのWebブラウザを使うのが大きな特徴で、そのためアプリケーション自体のフットプリントが小さくなります。


簡単な実例

Go 言語向けのラッパを例に、動作を確認してみます。

ここでは Go-WebUI の example アプリケーションを動かすことにしましょう。

以下でリポジトリをクローンします。

$ git clone https://github.com/webui-dev/go-webui.git

リポジトリにはセットアップスクリプトが用意されているので、これを実行します(mac または linux の場合は setup.sh、Windows Powershell の場合は setup.ps1 が用意されています)。

オプションは以下のものがあります。

$ ./setup.sh -h
Usage: setup.sh [flags]

Flags:
  -o, --output: Specify the output directory
  --nightly: Download the lastest nightly release
  --local: Save the output into the current directory
  -h, --help: Display this help message

このスクリプトは、webui のプラットフォーム別のリリースアーカイブをダウンロードして解凍・配備するだけのものです。

--local を指定した場合は、カレントのv2ディレクトリに、そうでない場合は GOPATH 配下のモジュール("$go_path/pkg/mod/$module@$version")として webuiが配備されます。


以下を実行します。

$ cd go-webui
./setup.sh --local

Windows の場合は以下を実行します。

> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
> .\setup.ps1 --local

v2/webui 配下に WebUI の実行バイナリが配備されます。


サンプルを v2 にコピーして実行してみましょう。

$ cp -r examples v2/
$ cd v2/examples
$ go run minimal.go

実行すると、ブラウザがキオスクモードで起動し、以下のように表示されます。

minimal.go は以下のようなコードになります。

package main

import "github.com/webui-dev/go-webui/v2"

func main() {
    w := webui.NewWindow()
    w.Show("<html>Hello World</html>")
    webui.Wait()
}

webui.NewWindow() で作成した WebUI ウインドウオブジェクトに対して Show で HTML を表示する簡単なものです。


Go からフロントエンドの呼び出し

サンプルが用意されているので実行してみましょう。

$ go run call_js_from_go.go

以下のウインドウが表示され、ボタン押下でカウントアップします。

call_js_from_go.go は以下のコードとなっています。

package main

import (
    "fmt"
    "strconv"
    ui "github.com/webui-dev/go-webui/v2"
)

// UI HTML
const doc = `<!DOCTYPE html>
<html>
  <head>
      <title>Call JavaScript from Go Example</title>
      <script src="webui.js"></script>
      <style>...略</style>
  </head>
  <body>
      <h1>WebUI - Call JavaScript from Go</h1>
      <br>
      <button id="MyButton1">Count <span id="count">0<span></button>
      <br>
      <button id="MyButton2">Exit</button>
      <script>
          let count = document.getElementById("count").innerHTML;
          function SetCount(number) {
              document.getElementById("count").innerHTML = number;
              count = number;
          }
      </script>
  </body>
</html>`

func myCountFunc(e ui.Event) any {
    count, _ := e.Window.Script("return count;", ui.ScriptOptions{})
    i, _ := strconv.Atoi(count)
    e.Window.Run(fmt.Sprintf("SetCount(%v);", i+1))
    return nil
}

func myExitFunc(e ui.Event) any {
    ui.Exit()
    return nil
}

func main() {
    w := ui.NewWindow()

    w.Bind("MyButton1", myCountFunc)
    w.Bind("MyButton2", myExitFunc)

    w.Show(doc)
    ui.Wait()
}

HTML <button id="MyButton1">...</button> のアクションに応答するには、以下の様に Go 関数に紐づけます。

w.Bind("MyButton1", myCountFunc)

myCountFunc は以下のようになっています。

func myCountFunc(e ui.Event) any {
    count, _ := e.Window.Script("return count;", ui.ScriptOptions{})
    i, _ := strconv.Atoi(count)
    e.Window.Run(fmt.Sprintf("SetCount(%v);", i+1))
    return nil
}

Window.Script では、JavaScriptを任意のウィンドウ上で実行します。これにより、値を読み込んだり、ビューを更新したりすることができます。 e.Window.Script("return count;", ui.ScriptOptions{}) ではカウントの値を取得しています。

Window.Run では、レスポンスを待たずにJavaScriptを素早く実行することができます。ここでは JavaScript で定義した SetCount 関数を呼び出してカウントを設定しています。


JavaScript から Go 関数の呼び出し

フロントエンド側で webui.handleStr() を以下のように定義することで、フロントエンド側からGo関数をコールすることができます。

<!DOCTYPE html>
<html>
    <head>
       <script src="webui.js"></script>
   </head>
    <body>
        <button onclick="webui.handleStr('Hello', 'World');">Call handle_str()</button>
    </body>
</html>

バックエンド側では以下のようにGo関数と紐づけることができます。

// JavaScript: `webui.handleStr('Hello', 'World');`
func handleStr(e ui.Event) ui.Void {
    str1, err := ui.GetArg[string](e)
    if err != nil {
        fmt.Println(err)
        return nil
    }

  str2, _ := ui.GetArgAt[string](e, 1)

    fmt.Printf("handleStr 1: %s\n", str1) // Hello
    fmt.Printf("handleStr 2: %s\n", str2) // World

    return nil
}

func main() {
    // ...
    ui.Bind(w, "handleStr", handleStr)
}


これらの例もサンプルがあるので以下により実行することができます。

$ go run call_go_from_js.go

以下のウインドウが表示され、JavaScript から Go 関数の呼び出しを確認できます。


その他にもテキストエディタの実装サンプルなどもあり、以下のように試すことができます。

$ cd text-editor
$ go run main.go


結局 WebUI とはなにものなのか

WebUI のソースは以下のように非常にシンプルです。

├─bridge
│      build.sh
│      js2c.py
│      utils.ts
│      webui_bridge.h
│      webui_bridge.ts
│
├─include
│      webui.h
│      webui.hpp
│
└─src
    │  webui.c
    │
    └─civetweb
            civetweb.c
            civetweb.h
            handle_form.inl
            match.inl
            md5.inl
            response.inl
            sha1.inl
            sort.inl

主に以下の3ソースで構成されます。

  • src/webui.c :WebUI バックエンド実装
  • include/webui.h:WebUI API
  • bridge/webui_bridge.ts: WebSocket でバックエンドと連携を行うフロントエンドライブラリ

webui.c では、C (C/C++) 組み込み Web サーバー civetweb(Mongoose からの MITフォーク)を利用した、WebSocket サーバと、ブラウザウインドウの管理を行います。civetweb ディレクトリには civetweb から必要なソースに絞ってコピーされたものが格納されています。

webui_bridge.ts は WebSocket のクライアント実装で、サーバとのやり取りは、WebUI で定義するバイナリデータでやり取りを行います。

つまり、WebUI は単に、「WebSocket によるRPC ライブラリ+ブラウザウインドウ管理を提供する」といったところになります。

ローカルで起動したWebサーバで、キオスクモードのブラウザとやり取りするだけのものです。


そういえば昔、WebSocket でサーバ側で画像を生成してクライアントに送信し、クライアントではサーバから受信した画像表示だけを行う形でテトリス作ったのを思い出しました。

blog1.mammb.com