【C++】DLLを作成する

DLLの作成方法に付いて解説していきたいと思う.より正確には静的ライブラリも含めて共有ライブラリの作成方法についてになると思うけど.

ライブラリの基本

そもそもライブラリってなんだろうかってことなんだけど、簡単に言ってしまえば事前にコンパイルが済んでいるパッケージだと思ってもらえばいいと思う。

ライブラリの種類

でだ,ライブラリには様々な種類と拡張子がある。

種類拡張子(Windows)拡張子(macOS)拡張子(Linux)
静的ライブラリlibsoa
動的ライブラリdlldylibso

リンクタイミング

さらに,ライブラリをリンクするタイミングにもいろいろある.

リンク方法使用する形式リンクタイミング
静的リンク静的ライブラリビルド時
動的リンク動的ライブラリ実行時
動的ロード動的ライブラリ呼び出し時

正確に言うと動的ロード,もといダイナミックロードはリンクに分類するかってのはちょっと怪しいけど,ライブラリ使用形態のひとつと考えればまぁ...

特徴としては,静的リンクに比べると動的リンクは実行時なのでファイルサイズが小さくなったり,dllを弄るだけで呼び出し側はコンパイルしなおさずとも処理を変更できる(その逆もまたしかり).ただ,実行がうまくいくかが実行時まで分からない(バイナリに直接リンクされないから呼び出されるべき関数が参照できるかどうかわからないという意味であってバグを引きにくいとか言った意味ではない)

ライブラリの作成

実際にライブラリを作成する方法を順を追って解説していこう

関数のexport設定

プログラムの実装方法は通常通りの手順で問題ないが,ライブラリに書き出したい関数(classとかも吐き出せるが今回は関数のみを扱う)の宣言時に修飾をする方法がある.しかもgccを使う場合とcsc(Visual Studioのコンパイラ)を使う場合とで異なった修飾形式となる

gccの場合

gccでコンパイルしてgccを書き出すには,関数の先頭に以下の修飾を行う.

__attribute__((visibility ("default")))

gccでコンパイルする場合,通常あればすべての関数が出力される仕組みになっているらしい.しかしそれでは隠蔽されないため困るケースが出てくるかと思う.そこでこの属性を付加することで,出力する関数を明示的に設定することができる.こうすると,この属性が付与されていない関数は隠蔽される.コンパイルオプションを追加することで隠蔽されるが,関数に明示的に属性を付加することで公開される.

cscの場合

cscってのはVisual Studioで使用しているコンパイラのこと.こいつはgccとは逆に,デフォルトで隠蔽されているためやはり出力用の属性付与してやらなければいけない.こんな感じ

__declspec(dllexport)

ネームマングリングに関する設定

C++で書いたプログラムのリンクに失敗した時,何やら関数名がとても複雑になっているのを見たことがある人はとても多いかと思う.そんな感じにある一定のルールに従ってシンボル名称を変更してしまうのがC++なんだけど,これには訳がある.というのも,C++では関数のオーバーロードが許容されていて,それを判別するために関数名や引数の型などからシンボル名称を決めている.

例えばclang++を用いて以下の関数をdllへと出力したとする.

$ vim libhello.cxx

__attribute__((visibility ("default"))) void hello(){
  std::cout << "Hello" << std::endl;
}

$ clang++ -o libhello.so -fPIC -shared dll.cxx
$ nm libhello.so | grep hello
0000000000001190 T _Z5hellov

ここで注目してほしいのは,関数名の頭に_Zがついて,その次にlen("hello"),が付いて,関数名の後ろにvoidを示すvが深されている.まぁ書き出された時点で関数名が変わってしまっているということ(正確にはコンパイルの時点)

これで困るのはダイナミックロードの時で,ダイナミックロードの時はdll内部のシンボル名を指定する.けどそんなもの(今回はhello)なんてものはない.すると当然エラーが帰ってくるわけだ.

これを防ぐために一番簡単なのはCリンケージするということである.つまりCの関数として定義してあげること.で,ここでどんな制約を受けるかというと,この範囲内では関数のオーバーロードはできないということ.namespaceを分離しても名前が同じだったらシンボル名が同じになるのでそれも駄目.Cリンケージ範囲の内外で同名なのはOK

早い話,Cリンケージってのはこんな感じでやる.

// 単体Cリンク
extern "C" void hello(){
  std::cout << "Hello" << std::endl;
}

// 範囲Cリンク
extern "C"{
  void hello(){
    std::cout << "Hello" << std::endl;
  }
}

ただし,例えCリンケージ範囲内であろうとclassのメンバは強制的にマングリングされるっぽい.これについては仕様をよく知らないので説明は控えさせてほしい.ただ,DLLの作成時に使用したヘッダーを呼び出し側でも使用するならclassのexportは大いにあり.そのほうがDLLも機能性を持たせやすいかと思う.

呼び出し規約の明示

C/C++だけでなく,たいていの言語には呼び出し規約というものが存在する.これがなにかというと,関数の呼び出し時に使用したコールスタックの巻き戻しをcaller(呼び出し側)が実行するかcallee(呼び出された側)が実行するかということである。

呼び出し規約にはさまざまな種類があるが,基本的には__stdcall__cdeclについて記憶しておけばよい.

前者はcalleeが,後者はcallerがコールスタックの操作を行うが,C/C++での規定は_cdecl,C#の規定は__stdcallとなっている.

そのため,言語をまたいでdllを使用するといった場合には特に注意する必要がある.もちろん言語間をまたがずとも,異なった規約で呼び出しを行ってしまった場合には突如としてローカル変数の値が書き換わったりと,発見しにくいバグの温床となってしまう.

さて,この呼び出し規約の明示方法だが,cscでもgccでも同様に以下のように指定すればよい.

void __stdcall function();
void __cdecl function();

DLL使用側が誤った呼び出し規約を指定しないためにも,DLLに書き出す関数に対しては規約の明示を行うとよいだろう.

コンパイル

さて,じゃあこんな感じのコードが書かれたcppファイルを動的ライブラリとしてかき出そうと思う.Visual Studioを使用している人は__attribute__なんちゃらを__declspecに読み替えて欲しい.

extern "C"{
  __attribute__((visibility ("default"))) int __cdecl add(int a, int b){
    return a + b;
  }

  __attribute__((visibility ("default"))) int __cdecl sub(int a, int b){
    return a - b;
  }

  __attribute__((visibility ("default"))) int __cdecl mul(int a, int b){
    return a * b;
  }

  __attribute__((visibility ("default"))) int __cdecl div(int a, int b){
    return static_cast<double>(a) / b; 
  }
}
</double>

これを動的ライブラリとしてコンパイルする.

clang++ -o libdll.so -fPIC -shared dll.cxx<br>

そういえばいい忘れてたんだけど,gccでは動的ライブラリの名前はlibなんちゃら.soって決められてる.

ライブラリの使用

今回はダイナミックロードに絞って話をしていく.もしダイナミックリンクをするって場合にはヘッダーに関数宣言を書くなりして呼び出し側で読み込んで関数の存在を明らかにしなきゃいけない.そんなのは面倒なんで今回はダイナミックロード.幸いなことに(?)この機能はWindowsAPIのほうが記事が多い.

DLLのロード

gccの場合

#include <dlfcn.h>

auto dll = dlopen("./libdll.so", RTLD_NOW);</dlfcn.h>

cscの場合

auto dll = LoadLibrary("dll.dll");

関数ポインタの取得

gccの場合

auto add = reinterpret_cast<int(*)(int, int)>(dlsym(dll, "add"));

cscの場合

auto add = reinterpret_cast<int(*)(int, int)>(GetProcAddress(dll, "add"));

実際のコード

これらを踏まえて,実際にDLL関数をダイナミックロードして呼び出すコードを書いてみたので参考にしてほしい

#include <iostream>
extern "C"{
#include <dlfcn.h>
}

int main(){
  auto dll = dlopen("./libdll.so", RTLD_NOW);
  if(dll == NULL) {   
    std::cout << dlerror();
    return -1;
    }

  auto add = reinterpret_cast<int(*)(int, int)>(dlsym(dll, "add"));
  if(add == NULL){
    std::cout << dlerror();
    dlclose(dll);
    return -1;
  }

  std::cout << add(2, 3);
  dlclose(dll);

  return 0;
}

ビルド(gcc)

$ clang++ -o nyan use_dll.cxx -ldl

dlとかいうライブラリをリンクしなきゃいかんらしい

つらつらと書き連ねてきたけどコレだけやればそれなりのDLLは記述できると思う

あわせて読みたい