Visual Studio で DLL を使うとき、.lib を 1 つ指定しただけなのに複数の DLL が絡む場合があります。MSDN の「1つのインポート ライブラリから複数の DLL を指定できる」とは何か、lib.exe と dumpbin を使って仕組みと手順を解説します。
結論:複数のインポート ライブラリを lib.exe で結合すればよい
「1つのインポート ライブラリ(.lib)から、複数の DLL を参照できるのか?」という疑問は、結局のところインポート ライブラリの正体が“普通のアーカイブ(入れ物)”であることを理解するとスッキリします。
MSVC 付属の lib.exe は、複数の .lib を 1つにまとめる(結合する)機能を持っています。まずは結論のコマンドからです。
LIB.EXE /OUT:c.lib a.lib b.lib
これで a.lib と b.lib に入っていたメンバ(オブジェクト)が、1つの c.lib にまとめられます。もし a.lib が a.dll のインポート ライブラリ、b.lib が b.dll のインポート ライブラリなら、結果の c.lib には両方の DLL を指すインポート情報が混在する形になります。
結合後の中身は dumpbin で確認できます(実行ファイル名は環境により dumpbin.exe です)。
dumpbin /headers c.lib
dumpbin /linkermember c.lib
/headers はメンバ(中に入っているオブジェクト)の種別や機械種別(x86/x64 など)を見るのに便利で、/linkermember は「この .lib が提供するシンボル」と「それがどのメンバに入っているか」を一覧できます。どの DLL 名に結び付いたインポート オブジェクトが入っているかも手掛かりになります。
インポート ライブラリ(.lib)の基礎:静的ライブラリとの違い
Visual Studio / MSVC の世界では、拡張子が同じ .lib でも、実態は大きく 2種類あります。
| 種類 | 中身のイメージ | リンク時に行われること | 実行時に必要なもの |
|---|---|---|---|
| 静的ライブラリ(static .lib) | 実装コードを含む .obj の集合 | 必要な .obj が取り込まれ、最終 EXE/DLL にコードが埋め込まれる | 追加の DLL は不要(EXE/DLL 内に実装が入る) |
| インポート ライブラリ(import .lib) | 「このシンボルはこの DLL から読み込む」という薄いオブジェクトの集合 | 最終 EXE/DLL のインポート テーブル(IAT/INT)に DLL 名とシンボルが記録される | 実行時に該当 DLL が必要(ローダが読み込む) |
ポイントは、インポート ライブラリはコード本体を持っているわけではなく、「どの DLL のどのエクスポートを使うか」をリンカに伝える“メタデータの束”である、ということです。
そしてもう一つ重要なのが、.lib はフォーマット上“アーカイブ”だという点です。アーカイブというのは、ざっくり言えば「複数の .obj を 1つのファイルに詰め込んだ入れ物」です。静的ライブラリはもちろん、インポート ライブラリも同じ“入れ物”の形式を使っており、内部には多数のメンバ(オブジェクト)が入っています。
「1つの .lib から複数 DLL を指定できる」の意味
MSDN の「バイナリ互換性のため、1つのインポート ライブラリから複数の DLL ファイルを指定できる」という説明を、現場の感覚に落とし込むと次の意味になります。
- インポート ライブラリの各メンバ(インポート オブジェクト)は、参照先の DLL 名を持てる
- 1つの .lib の中に、
msvcp140.dllを参照するメンバと、msvcp140_1.dllを参照するメンバが混在していても形式上まったく問題ない - リンカは「未解決シンボルを解決するために必要なメンバだけ」を .lib から選んで取り込むため、使ったシンボルに応じて最終的なインポート先 DLL が決まる
つまり、msvcprt.lib を 1つだけリンク指定しても、結果の EXE/DLL が msvcp140.dll と msvcp140_1.dll の両方を必ずインポートするわけではありません。実際に使った関数・クラス(=参照したシンボル)がどちらの DLL に割り当てられているかで、インポート テーブルに書かれる DLL 名が決まります。
なぜ CRT(C ランタイム)でこの仕組みが効くのか
CRT や STL の DLL は、長期運用で「互換性を壊さずに機能追加をしたい」という要件が強い領域です。既存の DLL(例:msvcp140.dll)のエクスポート集合を大きく変えずに、新しい実装や新しいシンボルを別 DLL(例:msvcp140_1.dll)へ移していけば、古いバイナリとの互換性を保ちつつ、更新や分離がしやすくなります。
そして開発者体験としては、リンク設定に複数の .lib を並べるのではなく、従来どおり 1つの “代表 .lib” を指定するだけで済むように、インポート ライブラリ側をまとめておく、という考え方になります。
実際の挙動:リンク時と実行時を混同しない
「.lib を追加したら DLL がリンクされる」という言い回しは便利ですが、ここは一度整理しておくとトラブルを避けられます。
| タイミング | 何が起きるか | 決め手になるもの |
|---|---|---|
| ビルド時(リンク時) | リンカが未解決シンボルを解決するため、.lib 内の必要なメンバだけを参照し、最終 EXE/DLL のインポート テーブルを作る | あなたのコードが参照したシンボル(関数名・クラスのメンバなど) |
| 実行時(ロード時) | Windows ローダがインポート テーブルに書かれた DLL 名を見て DLL を読み込み、IAT を解決する | リンク結果として埋め込まれた DLL 名とインポート一覧 |
この整理を踏まえると、「msvcprt.lib を 1つ指定したら msvcp140.dll と msvcp140_1.dll の両方が読み込まれるのか?」への答えは次のとおりです。
- あなたのプログラムが
msvcp140_1.dll側のシンボルを 1つでも参照した場合は、最終 EXE/DLL のインポート テーブルにmsvcp140_1.dllが載り、実行時にロードされます。 msvcp140.dll側のシンボルしか参照していない場合は、msvcp140_1.dllが .lib 内に存在していても、リンク結果に出てこない(=ロードされない)ことが普通です。
これが「1つのインポート ライブラリから複数の DLL を指定できる」の実務上の意味です。リンク設定としては .lib は 1つでも、最終成果物のインポート先 DLL は参照シンボルによって増減します。
手を動かして理解する:2つの DLL を 1つの .lib にまとめる手順
「lib.exe に複数の .def を渡して 1つの .lib を作れないのか?」という疑問はもっともですが、MSVC の lib.exe は“アーカイブを作る/結合する”ことが主用途で、複数の .def を直接統合してインポート ライブラリを生成するという設計にはなっていません。
そのため、現実的で再現性の高い手順は次の流れになります。
サンプル:a.dll と b.dll を作り、ab.lib にまとめる
まず、エクスポートを定義した .def を用意します(ここでは最小例です)。
a.def
LIBRARY a
EXPORTS
foo
b.def
LIBRARY b
EXPORTS
bar
次に、それぞれの DLL をビルドするときに /IMPLIB でインポート ライブラリの出力名を指定します。
link /dll /def:a.def /out:a.dll /implib:a.lib a.obj
link /dll /def:b.def /out:b.dll /implib:b.lib b.obj
最後に、生成された 2つのインポート ライブラリを lib.exe で結合します。
lib /out:ab.lib a.lib b.lib
これでアプリ側は ab.lib だけをリンク指定すれば、foo(a.dll)と bar(b.dll)の両方を同じ .lib から解決できるようになります。
dumpbin で「ab.lib が複数 DLL を指している」ことを確かめる
結合した .lib の内部が気になる場合は、次のように見ていくと理解が進みます。
dumpbin /linkermember ab.lib
/linkermember は「このライブラリはどの公開シンボルを提供するか」を表示します。foo と bar が別々のメンバとして存在すること、そしてそれぞれのメンバが“インポート用オブジェクト”であることを確認できます。
また、より深掘りしたい場合は、インポート テーブルが出来上がった最終成果物(EXE/DLL)に対して次を実行します。
dumpbin /imports yourapp.exe
ここで a.dll と b.dll が並んでいれば、「1つの .lib から、結果として複数 DLL をインポートした」ことがストレートに分かります。逆に foo だけを使うプログラムなら、a.dll のみが出ることも確認できるはずです。
内部はどうなっている?インポート ライブラリを“分解して考える”
インポート ライブラリは魔法ではありません。仕組みを言語化すると、次の要素に分解できます。
- .lib は COFF アーカイブで、複数のメンバ(オブジェクト)を格納できる
- インポート ライブラリに入っているメンバは、多くの場合COFF インポート オブジェクト(薄いメタデータ)
- 各インポート オブジェクトは、解決するシンボル名と参照先の DLL 名(例:
msvcp140.dll/msvcp140_1.dll)を持つ - リンカは未解決シンボルに一致するメンバだけをアーカイブから選び、最終成果物の .idata(インポート関連セクション)を構築する
ここで重要なのは「DLL 名を持つのは .lib 全体ではなく、メンバ単位」だというイメージです。だからこそ、1つの .lib の中に複数の DLL を参照するメンバが混ざっていても成立します。
C++ のマングル名が出てくる理由
C++ の場合、std::string のコンストラクタやメンバ関数などは、リンカが識別できるようにマングル名(例:??0string@std@@QEAA@XZ のような形)になります。インポート ライブラリのメンバは、そのマングル名に対して「この名前はこの DLL にある」と紐付いています。
つまり、msvcp140.dll と msvcp140_1.dll が分かれている場合も、「どのマングル名がどちらに所属しているか」という対応表が、最終的にはインポート ライブラリに埋め込まれていると考えると理解しやすいです。
なぜ lib.exe で結合できるのか(フォーマット上の理由)
lib.exe の結合は、特別な“インポート情報の再生成”ではなく、基本的にはアーカイブのメンバを 1つにまとめ直すだけです。インポート オブジェクトは単体で完結しており、DLL 名もそのオブジェクト内に書かれているため、メンバを同居させても破綻しません。
この性質のおかげで、「複数の .def から 1つの .lib を作りたい」という要求も、次の 2段階に分ければ実現できます。
- 各 DLL から、それぞれのインポート ライブラリを作る(
link /implibなど) - 出来上がった .lib を
lib.exeで結合する
msvcprt.lib は内部的にどうやって複数 DLL を扱っているのか
msvcprt.lib(または環境により名前が異なる CRT/STL の import lib)が、msvcp140.dll と msvcp140_1.dll の両方に関係するように見えるのは、要するにその 1つの .lib の中に、両方の DLL を参照するインポート オブジェクトが入っているからです。
ここで注意したいのは、「.lib が複数 DLL を扱う」こと自体は特別な機能ではなく、COFF アーカイブの仕様に自然に乗っているだけという点です。したがって、あなたが msvcp140.lib と msvcp140_1.lib を用意し、次のようにまとめた場合も、発想としては同じです。
lib /out:msvcprt.lib msvcp140.lib msvcp140_1.lib
この方法で“1つの代表 .lib に統合する”作り方は、検証としても実運用としても筋が通っています。
C++Builder 付属の implib は何が違うのか
C++Builder(Embarcadero)の implib は、MSVC の lib.exe と役割が少し違います。lib.exe は主に「アーカイブを作る・結合する」道具ですが、implib は「DLL や .def を元に、インポート ライブラリ(import .lib)を生成する」ことに寄った道具です。
そのため環境によっては、implib に複数の入力(DLL や .def)を並べて指定し、1つの .lib を出力する使い方ができます。概念的には次のようなイメージです(実際のオプション名はバージョンで差があるため、手元では implib -? などで確認してください)。
implib -c ab.lib a.def b.def
# あるいは
implib -c ab.lib a.dll b.dll
この 1行でやっていることは、実は MSVC でも同じで、工程がまとめられているだけです。
- 各入力(a/b)から「シンボル名」と「参照先 DLL 名」の対応を取り出す
- 対応表をインポート オブジェクトとして生成する
- それらを 1つの .lib(アーカイブ)にまとめる
つまり、implib が“Microsoft 標準ではない”という点はツールの違いであり、生成物の本質は「複数 DLL を指すインポート オブジェクトが同居した .lib」です。MSVC の世界で同じ状態を作りたいなら、前述のとおり「各 DLL の import lib を作ってから lib.exe で結合する」手順が最も安全で移植性も高いです。
実務で気を付けたい落とし穴
「結合すれば何でも OK」ではなく、運用上は次の点に注意すると事故を減らせます。
同名シンボルが複数 DLL に存在すると混乱の元
もし a.dll と b.dll が同じ名前のエクスポート(例:Initialize のような C API)を持つ場合、インポート ライブラリを 1つにまとめるとどちらの DLL に結び付くべきかが曖昧になります。リンカが片方を拾ってしまったり、重複定義として扱われたり、意図しないリンク結果になる可能性があります。
統合 .lib を作る運用では、基本的に「同じシンボル名を別 DLL で公開しない」設計にしておくのが安全です。
x86/x64(および Debug/Release)を混ぜない
インポート ライブラリは薄いとはいえ、機械種別(Machine)が異なるオブジェクトを混ぜるとリンクエラーの原因になります。結合前に dumpbin /headers で Machine を確認し、対象プラットフォームごとに統合 .lib を用意しましょう。
「全部の DLL を必ず読み込みたい」なら別の仕掛けが必要
統合 .lib の思想は「使ったシンボルだけインポートする」なので、参照していない DLL はロードされません。もしライセンスチェックやプラグイン機構などの都合で「必ず DLL を読み込みたい」要件がある場合は、明示的に関数を参照する、または LoadLibrary を使うなど、別の仕掛けが必要です。
まとめ:複数 DLL を 1つの .lib にまとめるのは“仕様どおり”
- インポート ライブラリ(.lib)は「インポート オブジェクトがたくさん入ったアーカイブ」であり、メンバ単位で参照先 DLL 名を持てる
- そのため、1つの .lib の中に
msvcp140.dll用とmsvcp140_1.dll用のインポート オブジェクトが混在しても問題ない - MSVC では
LIB.EXE /OUT:統合.lib a.lib b.libのように結合するのがシンプルで確実 - 最終的にどの DLL がインポートされ、実行時にロードされるかは、参照したシンボルに応じて自動的に決まる
- C++Builder の
implibが 1コマンドで出来ることも、本質は「複数 DLL を指すインポート オブジェクトを 1つの .lib に詰める」ことにある

コメント