BlinkGTK アプリ連携 API リファレンス — カスタムスキーム・メッセージング・スクリプト注入

バージョン: 1.2.0-build2
最終更新: 2026-07-30

English


このページについて

アプリと Web ページを 双方向に接続する API です。次のことができます。

各 API の説明には、シグネチャに加えて動作の詳細 (どのスレッドで・いつ・
誰がメモリを解放するか・エッジケースで何が起きるか) を記します。これらは
現行バージョンの実装を確認して書かれた記述で、将来のバージョンで改善される
可能性のある点は「現在の実装では」と明示しています。

使い方

/* "app" スキームを登録。app://... へのアクセスにアプリが応答する */
static char* on_app_scheme(BlinkWebView *view, const char *uri, gpointer data) {
    return g_strdup("<html><body><h1>アプリが返したページ</h1></body></html>");
}

blink_web_view_register_custom_scheme(view, "app", on_app_scheme, NULL);
blink_web_view_load_uri(view, "app://home/");

一覧

したいこと API
独自スキームを登録 (テキスト応答) blink_web_view_register_custom_scheme()
独自スキームを登録 (バイナリ応答 + MIME 指定) blink_web_view_register_custom_scheme_full()
JS → C のメッセージを受け取る blink_web_view_register_message_handler()
メッセージを GObject シグナルで受け取る (多重購読) message-received シグナル
メッセージハンドラを解除 blink_web_view_unregister_message_handler()
C → JS へメッセージを送る blink_web_view_send_message_to_page()
C から JavaScript を実行する blink_web_view_execute_javascript()
スクリプトを注入 blink_web_view_inject_user_script()
CSS を注入 blink_web_view_inject_user_stylesheet()
注入したものを全解除 blink_web_view_remove_all_user_scripts()

カスタム URL スキーム

独自 URL スキームの流れ。ページの遷移を BlinkGTK が登録済みスキームと照合し、C コールバックの返した内容がページとして表示される。

まず知っておく 3 つの事実

  1. スキーム名は事前登録制です。 使えるのは組み込みの 5 つ —
    app / blinkgtk / res / resource / ebook — と、起動前に環境変数
    BLINKGTK_EXTRA_SCHEMES (カンマ区切り) で追加した名前だけです。
    URL のスキーム分類はエンジン起動シーケンスの早期に確定するため、
    register_custom_scheme() で任意の名前を後から増やすことはできません
    (詳細は後述の「使えるスキーム名」)
  2. あらゆる読み込みで発火します。 トップレベルのナビゲーションだけでなく、
    ページ内の <img src="app://..."> などのサブリソース、そして JavaScript の
    fetch() / XHR でもコールバックが呼ばれます。だからカスタムスキームの上で
    SPA (単一ページアプリ) を丸ごと動かせます
  3. コールバックは GTK メインスレッドで同期実行されます。 ハンドラが返る
    まで UI は止まります。大きなファイルの復号やネットワーク待ちをハンドラ内で
    行うと、その間ウィンドウが固まります。重い準備は事前に済ませ、ハンドラでは
    メモリ上のデータを返すだけにするのが定石です

URI スキームを登録します。登録したスキームの URL への読み込みが発生すると、
ネットワークアクセスの代わりにコールバックが呼ばれ、返した文字列が応答に
なります。

シグネチャ:

typedef char* (*BlinkCustomSchemeCallback)(BlinkWebView* web_view,
                                           const char* uri,
                                           gpointer user_data);

void blink_web_view_register_custom_scheme(BlinkWebView* web_view,
                                           const char* scheme,
                                           BlinkCustomSchemeCallback callback,
                                           gpointer user_data);

バイナリを返せる版です。画像・音声・フォントなど、0x00 を含みうるデータは
こちらを使います。

シグネチャ:

typedef GBytes* (*BlinkCustomSchemeBytesCallback)(BlinkWebView* web_view,
                                                  const char* uri,
                                                  char** out_mime,
                                                  gpointer user_data);

void blink_web_view_register_custom_scheme_full(BlinkWebView* web_view,
                                                const char* scheme,
                                                BlinkCustomSchemeBytesCallback callback,
                                                gpointer user_data);

例: 画像をアプリから供給する

static GBytes* on_ebook_scheme(BlinkWebView *view, const char *uri,
                               char **out_mime, gpointer data) {
    if (g_str_has_suffix(uri, "/cover.jpg")) {
        gsize len = 0;
        gchar *bytes = NULL;
        if (g_file_get_contents("/path/to/cover.jpg", &bytes, &len, NULL)) {
            *out_mime = g_strdup("image/jpeg");
            return g_bytes_new_take(bytes, len);
        }
    }
    return NULL;  /* 空ページ (200) になる */
}

URI はどう渡ってくるか — パースはハンドラの責務

コールバックには ebook://book/chapter1.xhtml?page=2 のような URI 全体
渡ります。ホスト部・パス部の分解、クエリの解釈はハンドラの責務です。
実アプリで踏みやすい点が 3 つあります。

  1. percent-decode が必要です。 日本語などの非 ASCII ファイル名は
    %E8%A1%A8%E7%B4%99.xhtml のようにエンコードされて届きます。
    g_uri_unescape_string() で復元しないと、実ファイルと突き合わせたときに
    「あるはずのファイルが 404」になります
  2. パストラバーサルを拒否してください。 ファイルシステムと対応づける場合、
    .. を含むパスをそのまま通すと配信ルートの外を読まれます。
    判定は percent-decode の後で行います (%2e%2e 対策)
  3. クエリとフラグメントの扱いを決めてから削ってください。 パスだけで
    ルーティングするなら ? 以降は除去しますが、検索などクエリを使う機能が
    あるなら先に保存しておきます
static char* path_from_uri(const char *uri) {
    const char *p = strstr(uri, "://");
    if (!p) return NULL;
    const char *slash = strchr(p + 3, '/');          /* ホスト部を飛ばす */
    char *path = g_strdup(slash ? slash + 1 : "");
    char *q = strpbrk(path, "?#");                    /* クエリ・フラグメント除去 */
    if (q) *q = '\0';
    char *decoded = g_uri_unescape_string(path, NULL); /* 1: percent-decode */
    g_free(path);
    if (decoded && strstr(decoded, "..")) {           /* 2: traversal 拒否 */
        g_free(decoded);
        return NULL;
    }
    return decoded;  /* 配信ルートからの相対パス */
}

使えるスキーム名

スキーム 由来
app 汎用 (アプリ本体のコンテンツ)
ebook 電子書籍リーダー向け
res / resource 汎用リソース
blinkgtk 内部リソース参照
(任意の名前) 起動前に BLINKGTK_EXTRA_SCHEMES=myapp,plugin で追加

なぜ事前登録制か: スキームを「standard scheme (scheme://host/path
構造を持ち、相対 URL が解決できる)」「secure context (HTTPS 相当)」として
扱うための分類は、エンジン起動シーケンスの早い段階でしか登録できません。
register_custom_scheme() は起動後に呼ばれるため、名前の追加はできず、
事前登録済みの名前にハンドラを割り当てる操作になっています。

BLINKGTK_EXTRA_SCHEMES の制限 (現在の実装): 環境変数で追加した
スキームは、ページ内の fetch() からの利用が renderer 側の制限で
弾かれます (<img> などのサブリソースとナビゲーションは動きます)。
fetch() を使う SPA を配信する場合は組み込み 5 種のいずれかを
使ってください。

なぜ file:// ではなくカスタムスキームか

ローカルコンテンツは file:// でも表示できますが、file:// のページでは
fetch() が使えず、origin も特殊です。カスタムスキームは
secure context (HTTPS 相当) の実 origin として扱われるため、
fetch()・Service Worker・secure context 限定の Web API がそのまま動きます。
実際に、EPUB リーダー BlinkGTK-Readium は Readium の SPA と書籍データ全体を
ebook:// で配信することで、file:// では動かなかった fetch() ベースの
アプリケーションをそのまま動かしています。

動作の詳細

項目 現在の実装
発火する読み込み ナビゲーション / サブリソース (<img> 等) / fetch() / XHR
コールバックのスレッド GTK メインスレッド。同期実行 (返るまで UI が止まる)
呼ばれるプロセス アプリ (ブラウザ) プロセス。レンダラは別プロセスのまま
戻り値の所有権 エンジンに移る (g_free() / g_bytes_unref() される)
Content-Type out_mime 指定が最優先。無指定なら拡張子から推定
拡張子推定 .html/.htm→text/html、.css.js/.mjs.json.svg.png.jpg/.jpeg.webp.txt未知の拡張子は text/html
charset テキスト系 MIME には常に utf-8 が付く (変更手段なし)
NULL 返却 空ページ (HTTP 200・0 バイト)。404 にはならない
ステータスコード 常に 200。指定手段なし
リダイレクト 不可 (3xx を返す手段なし)。誘導したい場合は本文の HTML/JS で行う
同一スキームの再登録 黙って上書き (後勝ち)
登録の範囲 プロセス全体。複数 WebView が同じスキームを登録すると最後の登録が全 WebView に適用される

複数 WebView での注意: 登録の実体はプロセス全体で 1 つです。WebView ごとに
違う内容を返したい場合は、コールバック第 1 引数の web_view で分岐するか、
スキーム名を分けてください。また、現在の実装では WebView を破棄しても登録は
自動解除されません。破棄する前に callback=NULL で解除してください
(解除しないと、破棄済み WebView を指すハンドラが残ります)。

セキュリティの注意: 現在の実装は、要求の発行元 (どのページからの
サブリソース要求か) を検査しません。秘匿性の高いデータを返すハンドラでは、
外部コンテンツを同じ WebView に表示しない設計にするか、URI を厳密に検査して
ください。


JS ↔︎ C メッセージング

ページ内の JavaScript に window.blinkgtk オブジェクトが用意されます。

ページの JavaScript と C アプリのメッセージング。postMessage を C のハンドラで受け、send_message_to_page を addMessageHandler で受ける双方向。

方向 JavaScript 側 C 側
JS → C window.blinkgtk.postMessage('name', data) blink_web_view_register_message_handler() で受信
C → JS window.blinkgtk.addMessageHandler('name', fn) で受信 blink_web_view_send_message_to_page()

まず知っておく 3 つの事実

  1. window.blinkgtk はページの読み込み完了 (onload) 後に現れます。
    ページ先頭のインラインスクリプトが実行される時点ではまだ存在しません。
    ページ側の初期化は window.addEventListener('load', ...) の後、または
    存在確認をしてから行ってください
  2. JS 側のハンドラ登録はページ遷移で消えます (新しいドキュメントでは
    window が作り直されるため)。ページ側は遷移のたびに
    addMessageHandler を呼び直します。一方 C 側のハンドラは WebView に
    属し、ナビゲーションを跨いで生存
    します — 再登録は不要です
  3. C 側に届く data は Base64 のバイト列です。 g_base64_decode()
    復元してください。逆方向 (C → JS) は Base64 化されず生文字列が渡る
    非対称な設計です

JS → C のメッセージ受信ハンドラを登録します。

シグネチャ:

typedef void (*BlinkMessageCallback)(const char* name,
                                     const guint8* data,
                                     gsize length,
                                     gpointer user_data);

void blink_web_view_register_message_handler(BlinkWebView* web_view,
                                             const char* name,
                                             BlinkMessageCallback callback,
                                             gpointer user_data);

JavaScript 側から渡せるデータ (postMessage(name, data)data):

JS 側の型 挙動
文字列 UTF-8 として Base64 化。日本語も安全
ArrayBuffer バイナリのまま Base64 化 (バイト列が透過)
Uint8Array などの TypedArray ArrayBuffer と見なされず、JSON 文字列化 ({"0":72,...}) になります。.buffer を渡してください
object / 配列 / 数値 JSON.stringify して Base64 化。ただし JSON に非 ASCII 文字が含まれると例外になり届きません。日本語を含む object は自分で JSON.stringify して文字列として渡してください
null / undefined 空データ (length 0)

message-received シグナル (多重購読)

チャネル別コールバックとは別に、全メッセージが GObject シグナル
message-received としても発火します。g_signal_connect() は何個でも
接続できるため、ログ記録のような横断的な購読に向きます。

/* name: チャネル名、data: Base64 文字列 */
static void on_any_message(BlinkWebView *view, const char *name,
                           const char *data, gpointer user_data) {
    g_print("message on channel '%s'\n", name);
}
g_signal_connect(view, "message-received", G_CALLBACK(on_any_message), NULL);

シグナルはチャネル名でフィルタされません。購読側で name を判定して
ください。

登録したハンドラを解除します。

シグネチャ:

void blink_web_view_unregister_message_handler(BlinkWebView* web_view,
                                               const char* name);

解除後の挙動に注意: 解除したチャネル (および最初から未登録のチャネル)
へのメッセージは、エラーにならず黙って捨てられます。開発中に
「メッセージが届かない」ように見えたら、まずチャネル名の一致と登録の有無を
疑ってください。なお message-received シグナルは解除の影響を受けず
発火し続けます。

C からページ内の JavaScript へメッセージを送ります。

シグネチャ:

void blink_web_view_send_message_to_page(BlinkWebView* web_view,
                                         const char* name,
                                         const char* data);

ページ側は先にハンドラを登録しておきます。

<script>
window.addEventListener('load', function () {
  window.blinkgtk.addMessageHandler('page-turn', function (data) {
    console.log('アプリからの指示:', data);
  });
});
</script>

届かない条件に注意: この関数はキューを持ちません。ページの読み込みが
完了する前、またはページ側が addMessageHandler を呼ぶ前に送ったメッセージは
黙って捨てられます (戻り値もエラーもありません)。確実に届けるには、
ページ側から準備完了を通知させてから送るのが定石です:

/* ページ側: 準備ができたら ready を送る
 *   window.blinkgtk.postMessage('ready', '');
 * C 側: ready を受けてから送信を始める */
static void on_ready(const char *name, const guint8 *data,
                     gsize length, gpointer user_data) {
    BlinkWebView *view = BLINK_WEB_VIEW(user_data);
    blink_web_view_send_message_to_page(view, "config",
                                        "{\"theme\":\"dark\"}");
}
blink_web_view_register_message_handler(BLINK_WEB_VIEW(view), "ready",
                                        on_ready, view);

動作の詳細

項目 現在の実装
window.blinkgtk の出現時期 各ページの onload 完了後 (ナビゲーションごとに再注入)
C コールバックのスレッド GTK メインスレッド
C ハンドラの寿命 WebView と同じ。ナビゲーションを跨いで生存
JS ハンドラの寿命 ドキュメントと同じ。遷移ごとに再登録が必要
JS → C の符号化 Base64 (C 側で g_base64_decode())
C → JS の符号化 なし (生文字列が渡る)
ロード完了前の送信 (C → JS) 黙って破棄 (キューなし)
未登録チャネルへの送信 (JS → C) 黙って破棄
同名チャネルの再登録 上書き (両方向とも)
サイズ制限 エンジン側の明示的な制限なし。Base64 で約 1.33 倍に膨らむため、大きなデータはカスタムスキームでの配信を検討
iframe window.blinkgtk はメインフレームにのみ注入。same-origin の iframe からは parent.blinkgtk で到達可能

セキュリティの注意: postMessage はページ内のあらゆるスクリプトから
呼べます (発行元の検証はありません)。外部のコンテンツや第三者スクリプトを
表示する WebView では、受信データを信頼せず、C 側で必ず検証してください。


スクリプト・CSS の注入

JavaScript をページに注入します。

シグネチャ:

void blink_web_view_inject_user_script(BlinkWebView* web_view,
                                       const char* script,
                                       gboolean inject_at_document_start);
static void on_load(BlinkWebView *view, int ev, const char *uri, gpointer data) {
    if (ev == BLINK_LOAD_FINISHED) {
        blink_web_view_execute_javascript(view,
            "document.title = '[MyApp] ' + document.title;", NULL, NULL);
    }
}
g_signal_connect(view, "load-changed", G_CALLBACK(on_load), NULL);

CSS を現在のページと以後のページに注入します。

シグネチャ:

void blink_web_view_inject_user_stylesheet(BlinkWebView* web_view,
                                           const char* css);

読書アプリの「ダークテーマ」「行間の調整」のように、コンテンツ側を
書き換えずに見た目を変える用途に使えます。動作の詳細:

/* 冪等な差し替え: 前回の注入分を取り除いてから注入し直す */
blink_web_view_execute_javascript(view,
    "var e = document.getElementById('myapp-style'); if (e) e.remove();",
    NULL, NULL);
char *js = g_strdup_printf(
    "var s = document.createElement('style');"
    "s.id = 'myapp-style'; s.textContent = '%s';"
    "document.head.appendChild(s);", escaped_css);
blink_web_view_execute_javascript(view, js, NULL, NULL);
g_free(js);

登録済みのスクリプトとスタイルシートをすべて (関数名は scripts ですが
CSS も対象) 解除します。

シグネチャ:

void blink_web_view_remove_all_user_scripts(BlinkWebView* web_view);

効果は「以後のページに適用しない」ことです。 現在表示中のページに
既に注入された <style> は取り除かれません。即時に外したい場合は、上の
冪等パターンのように id を付けて注入しておき、execute_javascript()
remove() してください。


C から JavaScript を実行する

現在のページのメインフレームで JavaScript を実行します。

シグネチャ:

typedef void (*BlinkJavaScriptCallback)(const char* result, gpointer user_data);

void blink_web_view_execute_javascript(BlinkWebView* web_view,
                                       const char* script,
                                       BlinkJavaScriptCallback callback,
                                       gpointer user_data);

動作の詳細:

blink_web_view_execute_javascript(view,
    "JSON.stringify({title: document.title, y: window.scrollY})",
    on_result, NULL);

完全な例: 独自スキーム + 双方向メッセージング

app://home/ をアプリが供給し、ページの準備完了 (ready) を受けてから設定を
送り、ボタン押下の通知を受け取ります。

#include <blink_gtk/blink_gtk.h>
#include <gtk/gtk.h>

static char* on_app_scheme(BlinkWebView *view, const char *uri, gpointer data) {
    return g_strdup(
        "<html><body>"
        "<h1>アプリ供給ページ</h1>"
        "<button onclick=\"window.blinkgtk.postMessage('clicked','hello')\">"
        "C 側へ通知</button>"
        "<script>"
        "window.addEventListener('load', function () {"
        "  window.blinkgtk.addMessageHandler('config', function (d) {"
        "    document.body.style.background = d;"
        "  });"
        "  window.blinkgtk.postMessage('ready', '');"  /* 準備完了を通知 */
        "});"
        "</script>"
        "</body></html>");
}

static void on_message(const char *name, const guint8 *data,
                       gsize length, gpointer user_data) {
    if (g_strcmp0(name, "ready") == 0) {
        /* ページの準備ができた — ここからは send が確実に届く */
        blink_web_view_send_message_to_page(BLINK_WEB_VIEW(user_data),
                                            "config", "#fffbe6");
        return;
    }
    gsize out_len = 0;
    guchar *decoded = g_base64_decode((const gchar *)data, &out_len);
    g_print("ページから '%s': %.*s\n", name, (int)out_len, decoded);
    g_free(decoded);
}

int main(int argc, char **argv) {
    blink_gtk_init(&argc, &argv);

    GtkWidget *win = gtk_window_new();
    gtk_window_set_default_size(GTK_WINDOW(win), 800, 600);

    GtkWidget *view = blink_web_view_new();
    gtk_window_set_child(GTK_WINDOW(win), view);

    blink_web_view_register_custom_scheme(BLINK_WEB_VIEW(view), "app",
                                          on_app_scheme, NULL);
    blink_web_view_register_message_handler(BLINK_WEB_VIEW(view), "ready",
                                            on_message, view);
    blink_web_view_register_message_handler(BLINK_WEB_VIEW(view), "clicked",
                                            on_message, view);

    gtk_window_present(GTK_WINDOW(win));
    blink_web_view_load_uri(BLINK_WEB_VIEW(view), "app://home/");

    return blink_gtk_run_main_loop();
}

ビルド:

cc -o app-integration app-integration.c $(pkg-config --cflags --libs blinkgtk-0.1)

実戦での構成例: EPUB リーダー

EPUB リーダー BlinkGTK-Readium は、このページの API だけで
「Web アプリを GTK アプリの中で製品として動かす」構成を実現しています。
設計の参考になる要点:


制限事項 (現在のバージョン)

制限 回避策
inject_user_script(document_start=TRUE) が機能しない load-changed + execute_javascript() で代替 (上記)
カスタムスキームのステータスコードは常に 200 エラーは本文の HTML で表現
リダイレクト不可 本文の HTML / JS で誘導
BLINKGTK_EXTRA_SCHEMES のスキームで fetch() 不可 組み込み 5 スキームを使う
C → JS 送信はロード完了前は破棄 ready ハンドシェイク (上記)
スキーム登録がプロセス全体で共有 WebView ごとに内容を変えるなら web_view 引数で分岐、破棄前に解除

関連

変更履歴

バージョン 変更
v1.1.0 (2026-07-30) 実装検証に基づき全面加筆 — スレッド・タイミング・所有権・エッジケースの「動作の詳細」、ready ハンドシェイク等の実践パターン、既知の制限を明記。旧版の「NULL で 404」は実装と異なっていたため是正 (実際は空ページ 200)
v1.1.0 本ページ初版 (バイナリ対応スキームは v1.1.0、他は v1.0 系から提供)