インポートライブラリ(.lib)で複数DLLをリンクする方法|lib.exe統合とmsvcprt.libの仕組み

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 に詰める」ことにある

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次