Rで使うDLLの作り方
WAVE(音声データ)を扱うDLLを作りましたがいつの間にか動かなくなっていました。
R.EXEのインストール場所が、bin から bin/i386 に変更されたことによるもので、DLLをbin/i386 に置けば動作しました。
これを調べる過程で知ったことを書いておきます。
本来の R Extension の作り方
ネット上で見られるのは、RTools、MinGW/MSYSを使うと言うものでした。
結論は、RToolsをPerlを伴ってインストールして、DOS窓で作業するとDLLが作れます。
要点は、Rのインストールパスで、通常、C:\Program Files\R\R-2.12.0\bin のようになりますが、これを参照できないようです。
C:\R\ に、コピーしてDLLが作れました。コピーは、binのだけでなく、並列にあるincludeなどもコピーが必要でした。
R CMD SHLIB tmp.c
のようにして、tmp.dll が得られました。
パス表記の問題回避にMSYSを使うものと思います。しかし、Rのインストールディレクトリが直接させないことには変わりがありませんし、RTools相当にMinGWを設定するのも困難でした。
Visual StudioでDLL
今でも普通のDLLが使えるのか試してみました。特に問題ないようです。
- extern "C"
- {
- __declspec(dllexport) void mul(double *ret, double *a, double *b)
- {
- *ret = *a * *b;
- }
- }
これを、Rで、以下のようにして試しました。
- > dyn.load("R_DLL1.dll")
- > mul <- function(a,b){
- + .C("mul", ret=double(1),
- + as.double(a),
- + as.double(b)
- + ) $ret
- + }
- > x <- mul(3,4)
- > x
- [1] 12
要点は、R Extension は、Cの関数だと言うことです。
dumpbin /exports R_DLL1.dll
として、確認するとエクスポートされている名前が確認できます。
- 1 0 000110EB mul = @ILT+230(_mul)
もし、extern "C" {} で囲まない場合は、以下のようになります。
- 1 0 00011140 ?mul@@YAXPAN00@Z = @ILT+315(?mul@@YAXPAN00@Z)
UnicodeかSJISか
-
Visual Studioの方では、プロジェクトを作成するとUnicodeがデフォルトになっています。
-
Rは、スクリプトを保存するとSJISのファイルができました。
-
Rがスクリプトファイルを読むときは、自動認識されます。
BOM付のUTF-8もOKでした。
-
Cの関数が受け取るのはSJISでした。
-
スクリプトでは、SJISが使われているようです。
- > s <- "a\x82\xa0b"
- > s
- [1] "aあb"
WaveIO_Rの構成
1つのRスクリプトファイルを、2つのDLLからできています。
R.exe |
スクリプトファイルの実行 |
|
Rスクリプト
|
↓ |
・dllのロード
・R Extension .C(),.Call()
|
|
|
|
Wave16R0.dll
|
DLL(C++)
・Rからは、Cの関数を公開した普通のDLLに見える
・CLR C++の記述でC#のアセンブリを利用
|
↓ |
共通言語サポート(CLR) |
|
Wave16R1.dll
|
|
C#で作成したアセンブリ
|
- Rのスクリプトから呼び出せるのはCの関数です。
- C#は、.obj を作らず、直接C++とリンクできません。
したがって、個別のDLLになっています。
- 普通に作成すると、以下のように配置することになります。
これは、インストールされていないアセンブリは、アプリケーションの起動ディレクトリが探されるためです。
Rのインストールディレクトリ
(C:\Program Files\R\R-2.12.0\bin\i386)
|
WaveIO_R.R
Wav16R0.dll |
R.LIB
Extension で、Rの型を扱うためには、Rの持つ関数を呼び出すことになります。これには、R.DLLからR.LIBを作ります。
R.LIBの作り方
dumpbin /exports R.dll
で、R.dllのエクスポートの一覧が得られる。これをエディタで、
Exports:
関数名
:
に、編集しR.DEFとします。
lib /def:R.DEF /machine:x86 /out:R.lib
R.LIBについて R.LIBを使うのは、Rの型を扱うためで、double*などの基本的な型で済むなら不要です。
R.LIBはR.DLLから作るので、Rのバージョンに依存してしまいます。
もう一つ、欠点があります。Rの型を扱うためなので、ごく限られた関数だけを使いますが、これが単独で呼び出せません。
理由は良くわかりませんが、Rのスクリプト経由でないと動作しません。ExtensionだけをVisual Studioでデバッグしたいのですができませんでした。
Rの型をC#で扱うようにC#で書き直せば、スッキリしますが、これも別な形でRのバージョン依存することになります。
リフレクション
「参照の追加」を行えば、アセンブリの境界を意識することなくコーディングができます。
この場合、参照に追加したアセンブリは、自動的に検索されることになり、アプリケーションの起動ディレクトリ(R.EXEのあるディレクトリ)に置くことが必要になります。
「参照の追加」を行わず、アセンブリを絶対パスでロードできます。この場合は、型情報が参照できないので、すべてリフレクションで記述することになります。
- Assembly^ WavIO_R::GetAssembly()
- {
- try
- {
- Assembly^ a = Assembly::GetAssembly(WavIO_R::typeid);
- array<FileStream^>^ fs = a->GetFiles();
- String^ dir=Path::GetDirectoryName(fs[0]->Name);
- dir += "\\Wave16R1.dll";
- a = Assembly::LoadFile(dir);
- if(a == nullptr)
- Error("Loading error : " + dir);
- return a;
- }
- catch(System::Exception^ e)
- {
- Error(e->Message);
- return nullptr;
- }
- }
- ConstructorInfo^ WavIO_R::GetConstructor(
- Assembly^ assem, String^ className,
- int arg_count, int arg_index, Type^ arg_type)
- {
- Type^ t = assem->GetType(className);
- array<ConstructorInfo^>^ constructors = t->GetConstructors();
- if(constructors->Length==1)
- return constructors[0];
- if(constructors->Length<=0)
- return nullptr;
- for(int i=0;i<constructors->Length;i++)
- {
- array<ParameterInfo^>^ params = constructors[i]->GetParameters();
- if((params->Length==arg_count)&&(params[arg_index]->ParameterType==arg_type))
- return constructors[i];
- }
- return nullptr;
- }
- MethodInfo^ WavIO_R::GetMethod(Assembly^ assem, String^ className, String^ methodName)
- {
- Type^ t = assem->GetType(className);
- return t->GetMethod(methodName);
- }
- MethodInfo^ WavIO_R::GetMethod2(Assembly^ assem, String^ className, String^ methodName,
- int nParams, int param_index, Type^ param_type)
- {
- Type^ t = assem->GetType(className);
- array<MethodInfo^>^ mis = t->GetMethods();
- if((mis == nullptr) || (mis.Length<=0))
- return nullptr;
- // 複数ある
- for(int i=0; i<mis.Length; i++)
- {
- if(mis[i]->Name == methodName)
- {
- array<ParameterInfo^>^ pis = mis[i]->GetParameters();
- if(pis.Length == nParams)
- {
- if(pis[param_index]->ParameterType == param_type)
- return mis[i];
- }
- }
- }
- return nullptr;
- }
- PropertyInfo^ WavIO_R::GetProperty(Assembly^ assem, String^ className, String^ propertyName)
- {
- Type^ t = assem->GetType(className);
- return t->GetProperty(propertyName);
- }
- //----------------------------------------------------------------------------
- WavIO_R::WAVINFO WavIO_R::ReadFile(const char* path, int start, int length)
- {
- WAVINFO wi;
- Assembly^ assem = GetAssembly();
- // FileReaderをコンストラクト
- ConstructorInfo^ constructor= GetConstructor(assem, "RIFF.WaveReader", 1, 0, String::typeid);
- array<Object^>^ param=gcnew array<Object^>(1);
- param[0] = gcnew String(path);
- Object^ reader=constructor->Invoke(param);
- // オープン
- MethodInfo^ method = GetMethod(assem, "RIFF.WaveReader", "Open");
- method->Invoke(reader, nullptr);
- // フォーマットの取得
- PropertyInfo^ prop = GetProperty(assem, "RIFF.WaveReader", "Format");
- Object^ format = prop->GetValue(reader,nullptr);
- FieldInfo^ fi = format->GetType()->GetField("nSamplesPerSec");
- wi.hz = (int)(UInt32)(fi->GetValue(format));
- fi = format->GetType()->GetField("nChannels");
- wi.ch = (int)(UInt16)(fi->GetValue(format));
- // サンプルの総数
- prop = GetProperty(assem, "RIFF.WaveReader", "Samples");
- int samples = (int)(prop->GetValue(reader,nullptr));
- //読み込むサンプル数を計算
- int sc = samples - start;
- if(sc>length)
- sc = length;
- if(sc>0)
- {
- //実際に読み込みを行う
- wi.pcm_length = sc;
- try
- {
- // 読み込みメソッドを取得
- method = GetMethod(assem, "RIFF.WaveReader", "ReadSamples");
- //ここで読み出されるのは、16ビット x チャネル数が単位
- param=gcnew array<Object^>(2);
- param[0] = start;
- param[1] = wi.pcm_length;
- array<short>^ pcm = (array<short>^)(method->Invoke(reader, param));
- wi.pcm_length *= wi.ch;//R言語に返すサンプル数は16ビットが単位
- if(wi.pcm_length>0)
- {
- wi.pcm = (short*)malloc(wi.pcm_length * 2);
- IntPtr^ p = gcnew System::IntPtr(wi.pcm);
- System::Runtime::InteropServices::Marshal::Copy(pcm, 0, *p, wi.pcm_length);
- }
- }
- catch(System::Exception^ e)
- {
- Error(e->Message);
- return wi;
- }
- }
- // クローズ
- method = GetMethod(assem, "RIFF.WaveReader", "Close");
- method->Invoke(reader, nullptr);
- return wi;
- }
リフレクションでも型を参照したい
これは、interface に [TypeIdentifierAttribute]を付けることで実現できます。
任意のディレクトリからアセンブリ(DLL)をロード
C と CLI/C++
ここまでは、アセンブリ間の話しでしたが、Wav16R0.dllを作るには、C と CLI/C++間の結合があります。
この問題は、Cのヘッダファイルと、CLRのネームスペースの競合の話しです。
通常バイナリに翻訳されるCと、インタプリットされるCLI/C++が同時に成り立つ仕組みも不思議ですが、これは何の問題もないようです。
(出来上がったDLLを.ilにして確認すると、Cの関数もCLRによって公開されることが分かる。これはCの関数もilで記述可能であり、C#だけで目的が達せられる可能性を示唆する。)
ファイルの構成
Wave16R0.h (WaveIOクラスの定義) |
|
↓ |
⇒ |
Wave16R1.cpp |
|
C#のアセンブリを呼び出す
CLI/C++のクラス |
- Rの型を使うには、Rのヘッダファイルをインクルードすることになります。
同時に、一般的なヘッダファイルもインクルードされます。
using namespace System;
と、書くと競合が起きてコンパイルやリンクでエラーが起きます。
- Wave16R0.cppでRの型を扱うことにすると、CLRの型を避けることになります。
Wave16R1.cppのCLR部分とのインタフェースには、Cの型のみを使うことにします。
このインタフェースはWaveIO_Rクラスで行います。
- WaveIO_Rクラスは、条件コンパイルで、Wave16R0.cppに使われた場合は、CLRな型を隠します。
Wave16R0.h
- class WavIO_R
- {
- public:
- struct WAVINFO
- {
- int ch;
- int hz;
- int pcm_length;
- short* pcm;
- };
- private:
- #ifdef R_CLR
- static void DBG(String^ s)
- {
- TextWriter^ tw = gcnew StreamWriter("a4.txt",true);
- tw->WriteLine(s);
- tw->Close();
- }
- static Assembly^ GetAssembly();
- static ConstructorInfo^ GetConstructor(Assembly^ assem, String^ className, int arg_count, int arg_index, Type^ arg_type);
- static MethodInfo^ GetMethod(Assembly^ assem, String^ className, String^ methodName);
- static MethodInfo^ GetMethod2(Assembly^ assem, String^ className, String^ methodName,
- int nParams, int param_index, Type^ param_type);
- static PropertyInfo^ GetProperty(Assembly^ assem, String^ className, String^ propertyName);
- #endif
- public:
- #ifdef R_CLR
- static void Error(System::String^ s);
- #endif
- public:
- WavIO_R(void);
- ~WavIO_R(void);
- static void Play(int* pcm, int len, int ch, int hz);
- static void WriteFile(char* path, int* pcm, int len, int ch, int hz);
- static WAVINFO ReadFile(const char* path, int start, int samples);
- static WAVINFO Record(int ch, int hz, int sec);
- };
|