Closures as Win32 Window Procedures

2025/12/14 8:39

Closures as Win32 Window Procedures

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

改訂版要約:

この記事では、Win32 のウィンドウプロシージャに追加のコンテキストポインタを渡す方法を示しています。これは、WndProc が通常 4 つしか引数を取らないため、ネイティブ API には備わっていない機能です。著者は x64 アセンブラで小さなトランスペイル(trampoline)を作成し、実行時に JIT コンパイルして 5 番目の引数スロットを挿入し、呼び出し前に必要なコンテキストを格納します。これにより、各ウィンドウがグローバル変数や

GWLP_USERDATA
を使わずに独自の状態を保持できるようになります。トランスペイルは GNU アセンブラで書かれ、
.exebuf
セクション(
bwx
フラグ付き)から 2 MiB の実行可能バッファが確保されます。C ヘルパー関数
make_wndproc(Arena *, Wndproc5, void *arg)
は 2 つのバイトオフセットプレースホルダーを修正してトランスペイルを生成します。作成後は
set_wndproc_arg(WNDPROC p, void *arg)
を使ってコンテキストを変更できます。アロケータ例では、異なる状態オブジェクト用に複数のトランスペイルを生成したり、動的に切り替えたりする方法を示しています。この手法は、トランスペイルがアンウインドテーブルを持たないため Windows Control Flow Guard 下でも安全に機能し、グローバル変数を使わずにウィンドウごとのデータを付与する低レベルの手段を示しています。

本文

2025年12月12日

nullprogram.com/blog/2025/12/12/

2017 年に、JIT コンパイルされたラッパーを使って C でクロージャを作る技術について書きました。実際のプログラムではあまり必要になることは少なく、面白いものの、頻繁に思い出す機会がありませんでした。今回の投稿では、

qsort
に対してこの手法を適用し、残念ながらコンテキストポインタを受け取れない点が問題になっていました。実際には
qsort
は単なるデータのみで動作しますので、より実用的な使い道としては「カスタムアロケータのインタフェースが不十分な場合に、ランタイム時に特定の領域へ割り当てる関数を作ってくれれば…」というシナリオです。これまでの経験から得た知見を活かしつつ、最近の記事で再度検討した内容や、以前よりも改良できそうな点をまとめました。


Win32 ウィンドウプロシージャの拡張

Win‑32 の ウィンドウプロシージャ では、

LRESULT (???) 

というように全ての **先頭の四つのパラメータ―が全て 前後から…? …… 

We need to translate the entire article into Japanese, maintaining length. Provide a faithful translation.

Let's produce Japanese version.2025年12月12日

nullprogram.com/blog/2025/12/12/

2017 年に、JIT コンパイルされたラッパーを使って C でクロージャを作る技術について書きました。実際のプログラムではほとんど必要になることはなく、面白いものの頻繁に思い出す機会がありませんでした。今回の記事では、その手法を

qsort
に適用した経験を書いており、残念ながら
qsort
はコンテキストポインタを受け取れない点が問題になっていました。実際には単なるデータだけで動作するため、より実用的な使い道としては「カスタムアロケータのインタフェースが不十分な場合に、ランタイム時に特定の領域へ割り当てる関数を作ってくれれば…」というシナリオです。これまで得た知見と、最近読んだ記事で再考した内容や、以前よりも改良できそうな点をまとめました。


Win32 ウィンドウプロシージャの拡張

Win32 の ウィンドウプロシージャ は次のように宣言されます。

LRESULT Wndproc(
    HWND hWnd,
    UINT Msg,
    WPARAM wParam,
    LPARAM lParam);

ウィンドウを作るにはまず

RegisterClass
でクラスを登録し、そこに関数ポインタを渡します。

MyState *state = …;

RegisterClassA(&(WNDCLASSA){
    /* … */
    .lpfnWndProc   = my_wndproc,
    .lpszClassName = "my_class",
    /* … */
});

HWND hwnd = CreateWindowExA("my_class", …, state);

スレッドは OS からのイベントでメッセージ・プンプを走らせ、これをプロシージャにディスパッチします。プロシージャはその中でプログラム状態を操作します。

for (MSG msg; GetMessageW(&msg, 0, 0, 0);) {
    TranslateMessage(&msg);
    DispatchMessageW(&msg);   // ウィンドウプロシージャが呼ばれる
}

WNDPROC
の 4 つの引数は Win32 が決めており、コンテキストポインタを渡す仕組みはありません。従来、次の二通りで状態にアクセスしていました。

  • グローバル変数 – 汚いが簡単;チュートリアルでよく見かけます。
  • GWLP_USERDATA – ウィンドウに添付したポインタです。

後者はセットアップが必要です。Win32 は

CreateWindowEx
の最後の引数を、ウィンドウ作成時に
WM_CREATE
でプロシージャへ渡します。そのポインタは
CREATESTRUCT
を介して間接的に渡され、最終的には次のようになります。

case WM_CREATE:
    CREATESTRUCT *cs = (CREATESTRUCT *)lParam;
    void *arg = (struct state *)cs->lpCreateParams;
    SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)arg);
    /* … */

その後は

GetWindowLongPtr
で取り出せます。毎回この手順を経ると、もっと良い方法がないかと思ってしまいます。もし「ウィンドウプロシージャに第 5 引数を設けてコンテキストを渡す」ことができれば…という妄想です。

typedef LRESULT Wndproc5(HWND, UINT, WPARAM, LPARAM, void *);

ここではそのようなトランスペリ―(トンネル)を作るだけにします。x64 の呼び出し規約は最初の 4 引数をレジスタで、残りをスタックへ押し込みますが、新しい引数も同じくスタックに入ります。トランスペリ―は単に余分なパラメータをレジスタに置くわけではなく、実際にスタックフレームを構築します。少し手間ですが、それほど難しくはありません。


実行可能メモリの確保

前回の記事や今回使ったプログラムでは

VirtualAlloc
(または他環境なら
mmap
)で実行可能領域を確保していました。この方法だと、コードやデータから 2 GB を超えて離れた場所に割り当てられることがあり、相対アドレスで参照できなくなる問題があります。そこでより良い解決策として、ローダーから書き込み可能かつ実行可能なメモリブロックを取得し、その中でトランスペリ―を確保します。この領域は「実行可能」以外に特別な性質はなく、通常の割り当てと同じように扱えます。ローダー経由で確保すれば、ロード済みイメージ内にあるためほぼ常に近い位置にあり、JIT コンパイラは小さなコードモデルを仮定できます。

GNU 系ツールチェインで COFF をターゲットにした場合の一例です。

.section .exebuf,"bwx"
.globl exebuf
exebuf:   .space 1<<21          # 2 MiB の writable/executable メモリを確保

このアセンブリは

.exebuf
というセクションに 2 MiB を割り当てます。C 側では次のようにして取得します。

typedef struct {
    char *beg;
    char *end;
} Arena;

Arena get_exebuf(void)
{
    extern char exebuf[1<<21];
    Arena r = {exebuf, exebuf + sizeof(exebuf)};
    return r;
}

サイズを自分で書くのは面倒ですが、これだけ簡単に済みます。GCC のセクション属性で配列を定義したいところですが、その属性ではセクションフラグを設定できません。C コンパイラが GNU 系でなくても動作し、

exebuf
を含む小さな COFF オブジェクトを生成するだけなら十分です。

また、以下に必要となる他の基本定義もまとめておきます。

#define S(s)            (Str){s, sizeof(s)-1}
#define new(a,n,t)      (t *)alloc(a, n, sizeof(t), _Alignof(t))

typedef struct {
    char     *data;
    ptrdiff_t len;
} Str;

Str clone(Arena *a, Str s)
{
    Str r = s;
    r.data = new(a, r.len, char);
    memcpy(r.data, s.data, (size_t)r.len);
    return r;
}

これらは前回の記事で詳しく説明しています。


トランスペリ―・コンパイラ

ここからは、

Wndproc5
とバインドするコンテキストポインタを受け取り、従来の
WNDPROC
を返す関数を作ります。

WNDPROC make_wndproc(Arena *a, Wndproc5 proc, void *arg);

ウィンドウプロシージャは次のように第 5 引数で状態を受け取ります。

LRESULT my_wndproc(HWND, UINT, WPARAM, LPARAM, void *arg)
{
    MyState *state = arg;
    /* … */
}

クラス登録時には、

RegisterClass
に渡す関数ポインタとしてトランスペリ―を返します。

RegisterClassA(&(WNDCLASSA){
    /* … */
    .lpfnWndProc   = make_wndproc(a, my_wndproc, state),
    .lpszClassName = "my_class",
    /* … */
});

このクラスを使うウィンドウは、すべて第 5 引数で状態オブジェクトにアクセスできます。実際には

exebuf
の設定が最も手間でした;
make_wndproc
自体はとてもシンプルです。

WNDPROC make_wndproc(Arena *a, Wndproc5 proc, void *arg)
{
    Str thunk = S(
        "\x48\x83\xec\x28"      /* sub   $40,%rsp */
        "\x48\xb8........"      /* movq  $arg,%rax */
        "\x48\x89\x44\x24\x20"  /* mov   %rax,32(%rsp) */
        "\xe8...."              /* call  proc */
        "\x48\x83\xc4\x28"      /* add   $40,%rsp */
        "\xc3"                  /* ret */
    );
    Str r = clone(a, thunk);
    int rel = (int)((uintptr_t)proc - (uintptr_t)(r.data + 24));
    memcpy(r.data+6, &arg, sizeof(arg));
    memcpy(r.data+20, &rel, sizeof(rel));
    return (WNDPROC)r.data;
}

アセンブリは呼び出し側のスタックフレームを作り、シャドウスペースと新しい引数用領域を確保します。コンテキストポインタと 32‑ビット符号付き相対アドレス(

call
のオフセット)をパッチします。手動でオフセットを計算している点が唯一残念です。

トランスペリ―のポインタを保持すれば、いつでもコンテキストポインタを書き換えることができます。

void set_wndproc_arg(WNDPROC p, void *arg)
{
    memcpy((char *)p + 6, &arg, sizeof(arg));
}

例としては次のように使います。

MyState *state[2] = …;          /* 複数状態 */
WNDPROC proc = make_wndproc(a, my_wndproc, state[0]);
/* … */
set_wndproc_arg(proc, state[1]);   /* 状態を切り替える */

最もよくあるケースは複数のプロシージャを作ることです。

WNDPROC procs[] = {
    make_wndproc(a, my_wndproc, state[0]),
    make_wndproc(a, my_wndproc, state[1]),
};

トランスペリ―は Control Flow Guard(CFG)ポリシーが有効な状態でも動作します。スタックアンウィンドンエントリが無いので、Windows が制御を渡すのを拒否することはないと考えています。


より実用的なケース

GWLP_USERDATA
を使うより手間がかかりますし、実際のプログラムではウィンドウプロシージャの数は小さく固定(ほぼ 1 個)です。したがって最適な例とは言えませんが、実際に役立つ場面を示しました。例えば弱いカスタムアロケータインタフェースを持つライブラリの場合:

typedef struct {
    void *(*malloc)(size_t);   /* コンテキストポインタなし! */
    void  (*free)(void *);
} Allocator;

void *arena_malloc(size_t, Arena *);

Allocator perm_allocator = {
    .malloc = make_trampoline(exearena, arena_malloc, perm),
    .free   = noop_free,
};

Allocator scratch_allocator = {
    .malloc = make_trampoline(exearena, arena_malloc, scratch),
    .free   = noop_free,
};

将来に備えてこの手法をメモしておきます。

同じ日のほかのニュース

一覧に戻る →

2025/12/14 7:58

Linux Sandboxes and Fil-C

## Japanese Translation: メモリ安全性とサンドボックスはプログラムの異なる部分を保護するため、両方が強力なセキュリティに必要です。純粋な Java プログラムはメモリ安全であってもファイルシステムの syscalls を通じて任意のファイルを書き込むことができるし、逆にすべての能力を取り消したアセンブリプログラムでもメモリバグがある場合がありますが、カーネルが特権 syscalls を殺すためサンドボックスから逃げられません。サンドボックスは意図的に許容範囲を広く設計しているため、攻撃者は残されたメモリ安全性のバグを利用してブローカー・プロセスへ到達することができるので、両方の防御を組み合わせるとより強固な保護が得られます。 本書では、C/C++ 用に設計され、システムコールまで安全性を保証し、init や udevd などの低レベルコンポーネントで使用できるメモリ安全ランタイム「Fil‑C」への OpenSSH の seccomp ベース Linux サンドボックス移植方法について説明します。OpenSSH は既に chroot を採用し、`sshd` ユーザー/グループとして特権なしで実行し、`setrlimit` を使用し、非許可 syscalls を `SECCOMP_RET_KILL_PROCESS` で殺す seccomp‑BPF フィルタを適用しています。Fil‑C はその runtime 内で自動的にこれらの syscalls を許可することで簡素化します。背景スレッドは存続させつつスレッド生成を防ぐため、Fil‑C は API `void zlock_runtime_threads(void)` を追加し、必要なスレッドを事前確保してシャットダウンを無効にします。 OpenSSH の seccomp フィルタは強化されています。失敗時の挙動が `SECCOMP_RET_KILL` から `SECCOMP_RET_KILL_PROCESS` に変更され、mmap 許可リストに新たに `MAP_NORESERVE` フラグが追加され、`sched_yield` が許可されています。サンドボックスは二つの `prctl` コール(`PR_SET_NO_NEW_PRIVS` と `PR_SET_SECCOMP`)で構築され、エラー検出も行われます。Fil‑C のランタイムは `filc_runtime_threads_handshake` で全スレッドとハンドシェイクし、各スレッドが no_new_privs ビットと seccomp フィルタを持つことを保証します。複数のユーザー スレッドが検出された場合、安全エラーが発生します。 メモリ安全性とサンドボックスを組み合わせることで、OpenSSH はより厳格な隔離を実現し、メモリバグによる権限昇格リスクを低減します。このアプローチは他のセキュリティクリティカルプロジェクトにも採用を促す可能性があります。

2025/12/14 9:34

An Implementation of J

## Japanese Translation: ## 改訂版要約 本書は、技術仕様の構造化された目次であり、以下のように整理されています。 1. **第0章 – はじめに** 2. **第1章 – 文を解釈する** - 1.1 単語生成 - 1.2 構文解析 - 1.3 トレイン(列車) - 1.4 名前解決 3. **第2章 – 名詞** - 2.1 配列 - 2.2 型 - 2.3 メモリ管理 - 2.4 グローバル変数 4. **第3章 – 動詞** - 3.1 動詞の構造 - 3.2 ランク - 3.3 原子(スカラー)動詞 - 3.4 オブヴァース、同一性、および変種 - 3.5 エラー処理 5. **第4章 – 副詞と接続詞** 6. **第5章 – 表現** - 5.1 原子表現 - 5.2 ボックス化された表現 - 5.3 木構造表現 - 5.4 線形表現 7. **第6章 – ディスプレイ** - 6.1 数値表示 - 6.2 ボックス化表示 - 6.3 フォーマット済み表示 主要セクションの後に、付録A〜F(インキュナブルム、スペシャルコード、テストスクリプト、プログラムファイル、外国接続詞、およびシステム概要)が補足資料として提供されます。書末には参考文献・用語集・索引が付されています。 この構成(目次 → 詳細セクション → 付録 → 参照資料)は、読者に全体枠組みを最初に把握させたうえで、必要に応じて詳細へ掘り下げたり補足資料を参照したりできる明確かつ階層的な道筋を提供します。

2025/12/14 6:18

Recovering Anthony Bourdain's (really) lost Li.st's

## Japanese Translation: **概要:** 著者は、Greg TeChnoLogY の Anthony Bourdain リストから欠落した項目を復元するために、Common Crawl インデックスを照会する Python 3.14.2 スクリプト *commoncrawl_search.py* を作成しました。サイト全体をダウンロードする代わりに、スクリプトは検索語句と一致する URL プレフィックスのみを返す単一のインデックスリクエストを送信し、帯域幅を大幅に削減しクエリ速度を向上させます。この手法は標準的なオープンソース慣行に従っており、アーカイブ済みデータセットから失われた Web コンテンツを復旧する以前の試みを継承しています。今後はより複雑な検索に対応できるようツールを拡張し、リスト項目のさらなる自動復元を実現する可能性があります。成功すれば、研究者やファンがアクセスできるようになり、大規模公開アーカイブからデータを抽出する実用的な方法を示し、将来の Web 保存プロジェクトに情報を提供できるでしょう。