Home / ぼやきごと / 2013-08-27
2013-08-27

VC++:C++名前空間内の関数をDLLエクスポートして使う

どうにも私が文章を書くと無駄に長文になるきらいがあるので、DLLの作り方を知っている方向けに要点だけ先に書きます。

結論
C++名前空間内の関数をDLLエクスポートすることは可能。
方法
  • DLLのヘッダファイルとソースファイルには普通に名前空間を定義してその中にエクスポートしたい関数を定義する。
    ただし関数の呼び出し規約は __stdcall とすること。
  • DLL関数のエクスポートにはDEFファイルを使う。
    EXPORTS セクションには名前空間抜きの関数名(とその序数)を書く。
    例えば nspaceX::func 関数をエクスポートするなら func @1 といった感じで書く。
  • 静的リンクの場合、通常の場合と同様にできあがったインポートライブラリ(*.lib)をアプリにリンクするだけ。
    名前空間内のDLL関数をアプリのコードから普通に利用可能。
  • 動的リンクの場合、名前空間抜きの関数名がエクスポート名になっているのでその名前を使って関数アドレスを取得すればよい。
    もちろん関数名の代わりに序数を使っての取得も可能。
制限事項
  • 関数オーバロードは使えない。*1
  • 既にグローバルや別の名前空間で定義されている名前の関数はどちらもエクスポートできない。
    例えば nspaceX::func::func を両方定義してしまうとエクスポートする術がない。*2
その他
  • C++以外の言語でも使えるようにしたいなら、引数や戻り値に参照型(int& 等)は使わず、ポインタ型で代用すること。*3
    また、当然ながらC++のクラス型なども使わないこと。(構造体も言語によってはサポートしていないかも)
    ただしこれはエクスポートする関数の引数および戻り値についての話であり、DLL内部の実装にはいくらでも使ってOK。

要点は以上。
以降はもうちょっとだけ詳しく解説した長文です。

Visual C++ でDLLを作る際、他の言語からでも利用可能な汎用DLLとするには、次のことを守る必要があります。

  • クラスではなく、関数をエクスポートするようにする。
  • エクスポートする関数の呼び出し規約は __stdcall とする。*4
  • __declspec(dllexport) を使うのではなく、DEFファイルでエクスポートする関数を定義する。

ちなみに、DLLの作り方を解説しているサイトで「C++で作る場合は extern "C" が必要」という記述をよく目にしますが、これは __declspec(dllexport) を使う場合の話です。
DEFファイルを使って関数をエクスポートする場合、言語がC++であっても extern "C" は不要です。(付けてもいいですが)
ただし、それでも関数オーバロードは使えないと思った方がいいでしょう。(関数オーバロードをDEFファイルで解決する方法がわかりません…)

さて、関数をエクスポートするのはいいのですが、DLLの作り方を解説しているサイトの多くではグローバル名前空間の関数をエクスポートする例が載っています。
C++的には、不特定多数に公開する関数やクラスをグローバル名前空間に置くのは避けたいものです。
自分で定義した名前空間(namespace)内に置いた関数をエクスポートすることはできないのでしょうか。

結論から言えばできます
全く制約が無いわけではないですが、特に難しい記述をすることもなく普通にエクスポートできます。

以降の記述は Visual Studio 2012 Update3 での操作を前提としています。
古いバージョンの Visual Studio ではできないかもしれません。

まずはサンプルのDLLを作成してみましょう。

  1. 普通にDLL用のWin32プロジェクトを作成します。
    1. Visual C++ → Win32 → Win32 プロジェクト を選択し、プロジェクト名を入力。(ここでは sampleDll とします)
    2. アプリケーションの設定で「DLL」と「空のプロジェクト」にチェックし、他はチェックを外す。
  2. ヘッダファイルに sampleDll.h を追加して次のように記述します。
    すべて開くすべて閉じる
      1
      2
      3
      4
      5
      6
      7
      8
    
     
     
     
    -
    -
    !
    |
    !
    
    #pragma once
     
    namespace sampleDll
    {
        // エクスポートしたい関数群
        int __stdcall addAB(int a, int b);
        int __stdcall subAB(int a, int b);
    }
  3. ソースファイルに sampleDll.cpp を追加して次のように記述します。
    すべて開くすべて閉じる
      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
    
     
     
     
    -
    |
    -
    |
    !
    |
    |
    -
    |
    !
    !
    
    #include "sampleDll.h"
     
    namespace sampleDll
    {
        int __stdcall addAB(int a, int b)
        {
            return (a + b);
        }
     
        int __stdcall subAB(int a, int b)
        {
            return (a - b);
        }
    }
  4. ソースファイルにエクスポート用のDEFファイル sampleDll.def を追加します。
    ここにエクスポートしたい関数名を書くわけですが、名前空間は無視して、純粋な関数名だけを書くようにします。
    LIBRARY sampleDll
    EXPORTS
        addAB @1
        subAB @2
    
  5. プロジェクトのプロパティを開き、 リンカー → 入力 → モジュール定義ファイル の項目に sampleDll.def と記述します。
    Debug構成とRelease構成の両方に忘れずに設定してください。
  6. その他、必要に応じて設定を変更してください。
    特に Visual Studio 2012 ならば 全般 → プラットフォーム ツールセット の項目は v110_xpv100 にした方がいいでしょう。
  7. 後は普通にビルドすれば sampleDll.lib と sampleDll.dll ができあがります。

こうしてできあがったサンプルDLLの使い方ですが、静的リンク(sampleDll.lib を使ってリンク)するか動的リンク(実行時にDLLをロード)するかによって変わります。

静的リンク
普通のDLLと同じように、利用するアプリケーションのビルド時に sampleDll.lib をリンクするよう設定するだけです。
名前空間内のDLL関数をそのまま使えます。
動的リンク
各関数は、DEFファイルに書いたように、名前空間を無視した純粋な関数名でエクスポートされています。
なので例えばWin32APIの LoadLibrary 関数と GetProcAddress 関数を用いる場合は次のように記述すればOKです。
すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
-
!
 
 
 
-
!
 
 
-
-
!
|
-
-
!
|
|
-
-
!
!
|
-
!
!
|
|
!
// sampleApp.cpp
#include "sampleDll.h"
#include <Windows.h>
#include <iostream>
 
// 関数型の定義
typedef int (__stdcall* AddSubFunc)(int, int);
 
int main(int, char**)
{
    // DLLをロード
    ::HMODULE dll = ::LoadLibrary(TEXT("sampleDll.dll"));
    if (dll != 0)
    {
        // DLL関数を取得
        AddSubFunc add = reinterpret_cast<AddSubFunc>(::GetProcAddress(dll, "addAB"));
        AddSubFunc sub = reinterpret_cast<AddSubFunc>(::GetProcAddress(dll, "subAB"));
        if (add != 0 && sub != 0)
        {
            // 3 - (2 + 5) の結果を出力
            std::cout << sub(3, add(2, 5)) << std::endl;
        }
 
        // DLLを解放
        ::FreeLibrary(dll);
    }
 
    return 0;
}

制限事項などについてはこの記事の冒頭を参照してください。

なお、今回作成した sampleDll.dll のエクスポートテーブルを Visual Studio のツールである dumpbin で調べると、次のような感じに出力されます。

>dumpbin /exports sampleDll.dll
Microsoft (R) COFF/PE Dumper Version 11.00.60610.1
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file sampleDll.dll

File Type: DLL

  Section contains the following exports for sampleDll.dll

    00000000 characteristics
    521CAB3B time date stamp Tue Aug 27 22:35:55 2013
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00011023 addAB = @ILT+30(?addAB@sampleDll@@YGHHH@Z)
          2    1 0001101E subAB = @ILT+25(?subAB@sampleDll@@YGHHH@Z)

  Summary

        1000 .data
        1000 .idata
        2000 .rdata
        1000 .reloc
        1000 .rsrc
        4000 .text
       10000 .textbss

真ん中あたりを見ると、 addABsubAB がちゃんと名前空間付きでエクスポートされていることがわかると思います。

Category: [C++][Visual Studio][プログラミング] - 2013-08-27 23:11:54

*1 *2  厳密な関数名を指定できるならエクスポートできるかもしれませんが、試していません。
*3  そもそも参照型を使っても問題なくエクスポートできるのか試していません。
*4  VC++の WINAPICALLBACK も、元を辿れば __stdcall です。