C++での動的ローディングを試してみた。

C++で書かれたモジュールをdlopenして使いたいという話が出てきたので試行錯誤してみた記録です。

:::note warn
（追記）
動くには動いたものの、綺麗ではないなと思っていました。そもそも処理系依存ですし。コメントでも、必ずしも好ましい実装ではない、とのご指摘をいただきました。この項はあくまでご参考にとどめてください。よろしくお願いします。
:::

:::note warn
（言い訳がましくもう少し追記）
あと、これはx64のLinux上でg++を使ったもので、上にも処理系依存と書いてありますが、この方法でいつでも全部行けるというわけではありません。飽くまでも「こういう風に実現することもできるんですね」的な記事だと取ってください。まあ最悪「相手のライブラリをいじれない」「ヘッダファイル参照も避けたい」という状況があればこうなるという例であるとは思っててください。動的ローディングというのは、要らないところでは使わない方がいいテクニックですから…。
:::
# セットアップ
以下のようなコードを書いて呼び出される側として使います。
```C++:sample.h
#ifndef _SAMPLE_H_
#define _SAMPLE_H_
class Sample {
        private :
                int x;
                int y;
        public :
                Sample(int x, int y);
                int getX();
                int getY();
};
#endif
```
```C++:sample.cpp
#include "sample.h"
 
Sample::Sample(int _x, int _y) {
        x = _x;
        y = _y;
}
 
int Sample::getX() {
        return x;
}
 
int Sample::getY() {
        return y;
}
```
libsample.soを作成します。
```shell
% g++ -shared -o libsample.so sample.cpp
```
次に、これを普通に使うコードを書いてみます。
```C++:main.cpp
#include <stdlib.h>
#include <iostream>
#include "sample.h"
 
using namespace std;
 
int main(void) {
        Sample *s = new Sample(100,100);
        cout << s->getX() << endl;
        cout << s->getY() << endl;
}
```
コンパイルして実行してみます。（なお、どうでもいいことですがshellとしてはfish-shellを使っています。）
```shell
% g++ -o main.out main.cpp -L. -lsample
% set -x LD_LIBRARY_PATH .
% ./main.out
100
100
```
# 動的ローディングの試作1
ここまではうまく行ったので次にdlopen/dlsymを使ってみます。まずシンボル名が分からないと読み込めないので、nmを取ります。
```shell
% nm libsample.so
0000000000003e90 d _DYNAMIC
0000000000004000 d _GLOBAL_OFFSET_TABLE_
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000001122 T _ZN6Sample4getXEv
0000000000001136 T _ZN6Sample4getYEv
00000000000010fa T _ZN6SampleC1Eii
00000000000010fa T _ZN6SampleC2Eii
00000000000020f0 r __FRAME_END__
0000000000002000 r __GNU_EH_FRAME_HDR
0000000000004020 d __TMC_END__
                 w __cxa_finalize
00000000000010b0 t __do_global_dtors_aux
0000000000003e88 d __do_global_dtors_aux_fini_array_entry
0000000000004018 d __dso_handle
0000000000003e80 d __frame_dummy_init_array_entry
                 w __gmon_start__
000000000000114c t _fini
0000000000001000 t _init
0000000000004020 b completed.8061
0000000000001040 t deregister_tm_clones
00000000000010f0 t frame_dummy
0000000000001070 t register_tm_clones
```
これを基にシンボル名をハードコードしたものを書きます。
```C++:main_dl.cpp
#include <iostream>
#include <dlfcn.h>
#include <cstdint>
 
using namespace std;
 
int main(void) {
        dlerror();
        void* handle = dlopen("libsample.so",RTLD_LAZY);
        dlerror();
 
        void *gptr = dlsym(handle,"_ZN6SampleC1Eii");
        dlerror();
 
        typedef void* (*fptr)(int,int);
        fptr myptr = reinterpret_cast<fptr>(reinterpret_cast<intptr_t>(gptr));
 
        void* instance = (*myptr)(100,100);
 
        int (*getFunc)() = (int (*)())dlsym(handle,"_ZN6Sample4getXEv");
        dlerror();
        cout << (*getFunc)() << endl;
 
        getFunc = (int (*)())dlsym(handle,"_ZN6Sample4getYEv");
        dlerror();
        cout << (*getFunc)() << endl;
}
```
コンパイルして実行（LD_LIBRARY_PATHは既にセットされているので省略。）
```
% g++ -o main_dl.out main_dl.cpp -ldl
% ./main_dl.out
'./main_dl.out' terminated by signal SIGSEGV (Address boundary error)
```
駄目だ…。valgrindを掛けてみると、アドレスとして`0x64`を参照しようとしています。もしかして引数の一個目100がアドレスだと思われている？
そういえばJNIの規約でもCだと第一引数にjEnvが来てましたが…そういうことか。
# 動的ローディングの試作2
ということで書き直し。
```C++:main_dl2.cpp
#include <iostream>
#include <dlfcn.h>
#include <cstdint>
 
using namespace std;
 
int main(void) {
        dlerror();
        void* handle = dlopen("libsample.so",RTLD_LAZY);
        dlerror();
 
        void *gptr = dlsym(handle,"_ZN6SampleC1Eii");
        dlerror();
 
        typedef void* (*fptr)(void*,int,int);
        fptr myptr = reinterpret_cast<fptr>(reinterpret_cast<intptr_t>(gptr));
 
        void* dummy = malloc(1024);
        void* instance = (*myptr)(dummy,100,100);
 
        int (*getFunc)(void*) = (int (*)(void*))dlsym(handle,"_ZN6Sample4getXEv");
        dlerror();
        cout << (*getFunc)(instance) << endl;
 
        getFunc = (int (*)(void*))dlsym(handle,"_ZN6Sample4getYEv");
        dlerror();
        cout << (*getFunc)(instance) << endl;
}
```
`dummy`として1024byteの領域を確保してこれを第一引数に渡してやります。インスタンスメソッドも、どのインスタンスのメソッドを呼んでいるのか分からないと呼びようがないので、そういうことなんだろうな、と推測。
```shell
% g++ -g -o main_dl2.out main_dl2.cpp -ldl
% ./main_dl2.out
100
100
```
ということで無事に動作しました。
# 残った課題
ただ、これ、クラスのサイズが分からないので1024で決め打ちしてますが、この数字が外れだとデバッグが大変そうですね…。動的ローディングなのでコンパイラに領域サイズを尋ねることもできませんし…。
呼ばれる側をいじって良いのなら、クラスサイズを返すメソッドをクラス外に追加しても良いかも知れませんね…。

# 結論
C++のライブラリを動的ローディングで呼び出して使うことができるようになりました。