[Programming] DLLという名の黒魔術 ― Visual C++6.0のDLL作成 挫折からモダンモジュールへの系譜
1. 導入:なぜ今、DLLなのか?
限界までリソースを使い切った開発作業のあと、ふとYouTubeのおすすめに流れてきた配信動画があった。タイトルは「DLLの正体:ロードから破滅のアンロードまで」。サムネイルに描かれた少し見覚えのある三毛猫のイラストと、その不穏な文字列を目にした瞬間、私の脳裏に遠い昔の記憶がフラッシュバックした。
時代はWindows XP。開発環境といえばVisual C++ 6.0の、あの無骨でグレーなダイアログボックスが全盛だった頃の話だ。
C++の文法を覚え、自分なりのWindowsアプリケーションが組めるようになってくると、開発者は必ずある野望を抱く。「よく使う独自の計算ロジックや機能を一つのライブラリにまとめて、複数のアプリからスマートに使い回したい」と。そのためのWindowsにおける最適解が「DLL(Dynamic Link Library)」だった。
しかし、いざ自作しようとすると、そこには巨大な壁が立ちはだかっていた。当時のインターネットは今のように情報が整理されておらず、頼りの綱はMSDNの重厚で難解なドキュメントのみ。見よう見まねでコードを切り出してみても、謎のリンカエラーや実行時のクラッシュの嵐に見舞われる。「俺専用.dll」を作るという夢は、結局よくわからないまま、静かに挫折していった。
以前書いた記事『[Programming] Win32APIの今と昔 ― 古い技術が作用し続ける理由 において、古い技術が現代のOSの根底でどのように生き続けているかを考察した。DLLという概念もまた、決して過去の遺物として消え去ったわけではない。
あの時、私を打ちのめした「動的リンク」という黒魔術は、姿と名前を変え、今まさに私たちが日常的に触れているモダンな技術の中で脈々と生き続けているのだ。
[Programming] Win32APIの今と昔 ― 古い技術が作用し続ける理由 // PROTOCOL.LAIN
Win32APIはもはや古い技術なのか? 2000年以前に苦しみながら叩いたAPIが、25年後の今なお開発に役立っているという経験をもとに、Win32APIの思想と仕組み、そして現代的な技術(Web、Electron、WASMなど)との接続点を考察。自分の「過去のスキル」が無駄でなかったことを実感する記事。
lain-lab.com2. 黒魔術の正体:静的リンク(.lib)と動的リンク(.dll)
では、当時の私たちが「俺専用.dll」を作れなかった理由はどこにあったのか。
そもそもC++におけるライブラリには、コンパイル時に実行ファイル(EXE)の中にコードを丸ごと埋め込む「静的リンク(.lib)」と、実行時に外部ファイルから呼び出す「動的リンク(.dll)」の二種類が存在する。
静的リンクであれば、単に関数を別ファイルに書いてビルド時に繋げるだけで済む。しかし、動的リンクとなると話は全く別だった。単にコードを分割するだけでは、外部から関数を呼び出すことはできないのだ。
DLLを作るための最初の壁、それは「エクスポート(公開)」の概念だった。
DLL内に書かれた関数群の中から、外部のアプリに使わせたい関数だけを明示的に指定しなければならない。そのためには、関数の宣言部分に __declspec(dllexport) という長たらしくて直感に反する修飾子(おまじない)を付与するか、.def(モジュール定義ファイル)と呼ばれる専用のテキストファイルに公開する関数名を列挙する必要があった。
そして、ようやくビルドを通した後に待ち受けているのが、「呼び出し規約(Calling Convention)」という最悪の罠である。
関数を呼び出す際、引数をスタックに積む順番(右からか左からか)や、スタックの解放を呼び出し側が行うか呼ばれた側が行うかという「メモリ上の約束事」が存在する。WindowsのAPI標準である __stdcall と、C/C++標準の __cdecl。この規約が呼び出し側とDLL側で少しでも食い違っていると、コンパイルは通るのに、実行した瞬間にメモリが破壊されて即クラッシュする。
さらに、当時のWindows環境には「DLL Hell(DLL地獄)」と呼ばれる構造的なカオスが存在していた。
当時は多くのアプリケーションが、共通のDLLを C:\Windows\System32 のようなシステムフォルダに放り込んで共有していた。もし新しいアプリをインストールした際、そこに含まれる古いバージョンの共有DLLが、システムフォルダの新しいDLLを上書きしてしまったらどうなるか。
昨日まで正常に動いていた無関係な他のアプリたちが、関数が見つからない、あるいは仕様が変わったために一斉に死滅するのだ。
エクスポートのおまじない、呼び出し規約による即死トラップ、そしてシステム全体を巻き込むDLL Hellの恐怖。当時のDLL開発は、文字通り「OSの深淵を覗き込む黒魔術」だったのである。
3. 深淵を覗く:LoadLibrary() とメモリの振る舞い
おすすめに流れてきたあの配信動画が最も熱を帯びていたのは、まさにこの「OSの深淵」とも言える低レイヤーの振る舞いを解説している場面だった。
私たちが普段、何気なくアプリケーションを使っている裏側で、動的リンクはどのように実現されているのか。
C++やWin32APIの世界において、その引き金となるのが LoadLibrary() という関数だ。コード内でこの関数が呼び出された瞬間、OS(Windows)の内部では極めて泥臭く、そして精緻な「動的マッピング」の儀式が始まる。
まず、OSのメモリマネージャがストレージ上の .dll ファイルを探し出し、その中身(コードやデータ)を物理メモリ上にロードする。そして、実行中のアプリケーション(プロセス)が持つ「仮想メモリ空間」の一部に、そのDLLの領域をそっくりそのままマッピング(投影)するのだ。
これにより、元々は別々のファイルだったEXEとDLLが、実行時には「ひとつの地続きのメモリ空間」として結合されることになる。
しかし、空間が繋がっただけではまだ関数は呼べない。目的の処理がメモリ上の「どこ(何番地)」にあるのかが分からないからだ。
そこで登場するのが GetProcAddress() である。開発者は使いたい関数名を文字列で渡し、メモリ上に展開されたDLL内の目次(エクスポートテーブル)を検索させる。そして、該当する関数の「メモリアドレス(ポインタ)」を直接取得するのだ。
高度に抽象化された現代のプログラミングとは対極にある、「メモリの番地を直接探り当てて、実行経路を手作業で繋ぎ合わせる」という、ヒリヒリするような行為である。
そして、動画のタイトルにもあった「破滅のアンロード」。これこそが低レイヤー最大の恐怖だ。
用が済んだDLLは、FreeLibrary() という関数を呼んでメモリから解放(アンロード)しなければならない。しかし、もし解放した後に、手元に残っていた「関数のポインタ」を誤って実行してしまったらどうなるか?
ポインタが指し示すアドレスには、もう何もない。あるいは、既に全く別の無関係なデータで上書きされているかもしれない。 その瞬間、CPUは無効なメモリ領域への不正アクセスを検知し、「Access Violation(アクセス違反:0xC0000005)」という致命的エラーを吐き出す。OSは容赦なくプロセス全体を道連れにして、即座にアプリケーションを強制終了(クラッシュ)させる。
ガベージコレクションが不要なメモリを自動で掃除してくれる現代の言語とは違う。メモリの確保、配置の特定、そして解放まで、全てを開発者自身が完璧に制御しなければならない。一つでもポインタの扱いを間違えれば即座に破滅が訪れる。この「メモリを直接触る泥臭さと恐ろしさ」こそが、低レイヤー開発の魔境であり、同時に抗いがたい魅力でもあったのだ。
4. モダン技術への転生:Three.js は現代の「Three.dll」である
では、この難解極まる「動的リンク」という黒魔術は、過去の遺物として完全に消え去ったのだろうか? いや、決してそうではない。形を変え、レイヤーを変えて、現在の私たちが最もよく触れるモダンな開発環境のド真ん中で息づいている。
例えば、現代のフロントエンド開発において、私たちは息をするように他人のコードを再利用している。JavaScriptの ES Modules がその代表だ。
import { Scene, PerspectiveCamera, WebGLRenderer } from 'three';
WebXRの実装で日常的に書いているこの一行は、概念的にはまさしく「実行時(ブラウザでの評価時)に外部のライブラリをリンクする」行為そのものだ。
かつて __declspec(dllexport) という難解な呪文を書いていたエクスポート処理は、今やシンプルに export という数文字で安全に行えるようになっている。
視点を変えてみよう。今、私がAstroで構築したサイトで動かしている Three.js。これは見方を変えれば、ブラウザという名の巨大なOS上で、実行時に動的リンクされる 「Three.dll」 そのものではないか。
ネットワーク越しにダウンロードされ、V8エンジンのメモリ空間に展開され、私たちの書いたロジックから関数が呼び出される。そこには「呼び出し規約」の違いによる理不尽なクラッシュも、システムフォルダを破壊する「DLL地獄」も存在しない。高度に抽象化された、安全で美しいダイナミックリンクの世界が構築されている。
さらに、この概念を最も純粋な形で受け継いでいるのが WebAssembly (Wasm) だ。
C++やRustなどの低レイヤー言語で書かれたコードをコンパイルし、ブラウザ上でネイティブに近い速度で実行する技術である。JavaScript側から WebAssembly.instantiateStreaming() を使って .wasm ファイルを読み込むその所作は、OSが LoadLibrary() を使ってメモリにバイナリを展開するプロセスと完全に一致している。
かつてVC++ 6.0で四苦八苦しながらコンパイルしていたネイティブコードの資産やアルゴリズムは今、Webという巨大なプラットフォーム上で、真にポータブルで安全な「動的リンクモジュール」として転生を果たしたのだ。
あの時、手探りで作ろうとして作れなかった「俺専用.dll」の夢は、決して無駄に散ったわけではない。 パッケージマネージャ(npm)を通じて世界中の開発者が共有する巨大なエコシステムの中に、その思想は完璧に引き継がれているのである。
5. 結び:低レイヤーの知識は無駄にならない
Astroのような抽象度の高いモダンフレームワークや、Three.jsのような洗練されたライブラリを使っていると、私たちが普段書いているコードがいかにハードウェアから遠く離れた安全な場所にあるかを実感する。
ガベージコレクションがメモリを自動で掃除し、モジュールシステムが依存関係を完璧に解決してくれる。そこにはポインタのクラッシュも、メモリの解放忘れによる「破滅のアンロード」も存在しない。
しかし、その安全なレイヤーの足元では、OSが確実に LoadLibrary() のような泥臭い処理を行い、絶え間なくメモリ空間へのマッピングと番地の解決を行っている。
では、全てが隠蔽された現代において、低レイヤーの知識は無用なのだろうか? 私はそうは思わない。
WebXRのような極限のパフォーマンスが求められる領域や、複雑な非同期処理が絡み合うシステムにおいて、最後にものを言うのは「その下で何が起きているか」を想像できる力だ。 原因不明のメモリリークやパフォーマンスの低下に直面したとき、「あの中でポインタがどう振る舞い、メモリがどう確保されているか」という低レイヤーの泥臭いメンタルモデルを持っていることは、デバッグにおける最強の武器になる。
学生時代、MSDNのドキュメントを前に手探りで作ろうとし、結局完成しなかった「俺専用.dll」。
あの時の挫折感は、決して無駄ではなかった。あの時作れなかったDLLの概念と魂は、今、私が毎日エディタに向かって書いている TypeScript の export と import の中で、確かに息づいているのだから。
過去の記事:[Programming] Win32APIの今と昔 ― 古い技術が作用し続ける理由 で触れたように、古い技術は消え去るのではなく、ただ姿を変えて根底に沈んでいくだけだ。
深夜、冷めた厚揚げをつまみながら眺める低レイヤーの解説動画は、そんな技術の壮大な連続性と、自分が歩んできた道筋を再確認させてくれる、極上の時間だった。
[Programming] Win32APIの今と昔 ― 古い技術が作用し続ける理由 // PROTOCOL.LAIN
Win32APIはもはや古い技術なのか? 2000年以前に苦しみながら叩いたAPIが、25年後の今なお開発に役立っているという経験をもとに、Win32APIの思想と仕組み、そして現代的な技術(Web、Electron、WASMなど)との接続点を考察。自分の「過去のスキル」が無駄でなかったことを実感する記事。
lain-lab.com