mikeo_410


 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が使えるのか試してみました。特に問題ないようです。

  1. extern "C"
  2. {
  3.     __declspec(dllexport) void mul(double *ret, double *a, double *b)
  4.      {
  5.          *ret = *a * *b;
  6.      }
  7. }

  これを、Rで、以下のようにして試しました。

  1. > dyn.load("R_DLL1.dll")
  2. > mul <- function(a,b){ 
  3. +     .C("mul", ret=double(1),
  4. +       as.double(a),
  5. +       as.double(b)
  6. +       ) $ret
  7. +  }
  8. > x <- mul(3,4)
  9. > x
  10. [1] 12

  要点は、R Extension は、Cの関数だと言うことです。
    dumpbin /exports R_DLL1.dll
  として、確認するとエクスポートされている名前が確認できます。

  1. 1    0 000110EB mul = @ILT+230(_mul)

  もし、extern "C" {} で囲まない場合は、以下のようになります。

  1. 1    0 00011140 ?mul@@YAXPAN00@Z = @ILT+315(?mul@@YAXPAN00@Z)

UnicodeかSJISか

  1. Visual Studioの方では、プロジェクトを作成するとUnicodeがデフォルトになっています。
  2. Rは、スクリプトを保存するとSJISのファイルができました。
  3. Rがスクリプトファイルを読むときは、自動認識されます。
    BOM付のUTF-8もOKでした。
  4. Cの関数が受け取るのはSJISでした。
  5. スクリプトでは、SJISが使われているようです。
  1. > s <- "a\x82\xa0b"
  2. > s
  3. [1] "aあb"

WaveIO_Rの構成

  1つのRスクリプトファイルを、2つのDLLからできています。

R.exe

Rインタプリタ

スクリプトファイルの実行

WaveIO_R.R

Rスクリプト

・dllのロード
・R Extension .C(),.Call()

Wave16R0.dll

DLL(C++)
・Rからは、Cの関数を公開した普通のDLLに見える
・CLR C++の記述でC#のアセンブリを利用

共通言語サポート(CLR)

Wave16R1.dll

C#で作成したアセンブリ

  1. Rのスクリプトから呼び出せるのはCの関数です。
  2. C#は、.obj を作らず、直接C++とリンクできません。
    したがって、個別のDLLになっています。
  3. 普通に作成すると、以下のように配置することになります。
    これは、インストールされていないアセンブリは、アプリケーションの起動ディレクトリが探されるためです。

Rのインストールディレクトリ
(C:\Program Files\R\R-2.12.0\bin\i386)

Rのカレントディレクトリ
(getwd()で表示されるディレクトリ)
Wav16R1.dll 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のあるディレクトリ)に置くことが必要になります。
  「参照の追加」を行わず、アセンブリを絶対パスでロードできます。この場合は、型情報が参照できないので、すべてリフレクションで記述することになります。

  1. Assembly^ WavIO_R::GetAssembly()
  2. {
  3.     try
  4.     {
  5.         Assembly^ a = Assembly::GetAssembly(WavIO_R::typeid);
  6.         array<FileStream^>^ fs = a->GetFiles();
  7.         String^ dir=Path::GetDirectoryName(fs[0]->Name);
  8.         dir += "\\Wave16R1.dll";
  9.         a = Assembly::LoadFile(dir);
  10.         if(a == nullptr)
  11.             Error("Loading error : " + dir);
  12.         return a;
  13.     }
  14.     catch(System::Exception^ e)
  15.     {
  16.         Error(e->Message);
  17.         return nullptr;
  18.     }
  19. }
  20. ConstructorInfo^ WavIO_R::GetConstructor(
  21.     Assembly^ assem, String^ className, 
  22.     int arg_count, int arg_index, Type^ arg_type)
  23. {
  24.     Type^ t = assem->GetType(className);
  25.     array<ConstructorInfo^>^ constructors = t->GetConstructors();
  26.     if(constructors->Length==1)
  27.         return constructors[0];
  28.     if(constructors->Length<=0)
  29.         return nullptr;
  30.     for(int i=0;i<constructors->Length;i++)
  31.     {
  32.         array<ParameterInfo^>^ params = constructors[i]->GetParameters();
  33.         if((params->Length==arg_count)&&(params[arg_index]->ParameterType==arg_type))
  34.             return constructors[i];
  35.     }
  36.     return nullptr;
  37. }
  38. MethodInfo^ WavIO_R::GetMethod(Assembly^ assem, String^ className, String^ methodName)
  39. {
  40.     Type^ t = assem->GetType(className);
  41.     return t->GetMethod(methodName);
  42. }
  43. MethodInfo^ WavIO_R::GetMethod2(Assembly^ assem, String^ className, String^ methodName, 
  44.     int nParams, int param_index, Type^ param_type)
  45. {
  46.     Type^ t = assem->GetType(className);
  47.     array<MethodInfo^>^ mis = t->GetMethods();
  48.     if((mis == nullptr) || (mis.Length<=0))
  49.         return nullptr;
  50.     // 複数ある
  51.     for(int i=0; i<mis.Length; i++)
  52.     {
  53.         if(mis[i]->Name == methodName)
  54.         {
  55.             array<ParameterInfo^>^ pis = mis[i]->GetParameters();
  56.             if(pis.Length == nParams)
  57.             {
  58.                 if(pis[param_index]->ParameterType == param_type)
  59.                     return mis[i];
  60.             }
  61.         }
  62.     }
  63.     return nullptr;
  64. }
  65. PropertyInfo^ WavIO_R::GetProperty(Assembly^ assem, String^ className, String^ propertyName)
  66. {
  67.     Type^ t = assem->GetType(className);
  68.     return t->GetProperty(propertyName);
  69. }
  70. //----------------------------------------------------------------------------
  71. WavIO_R::WAVINFO WavIO_R::ReadFile(const char* path, int start, int length)
  72. {
  73.     WAVINFO wi;
  74.     Assembly^ assem = GetAssembly();
  75.     // FileReaderをコンストラクト
  76.     ConstructorInfo^ constructor=  GetConstructor(assem, "RIFF.WaveReader", 1, 0, String::typeid);
  77.     array<Object^>^ param=gcnew array<Object^>(1);
  78.     param[0] = gcnew String(path);
  79.     Object^ reader=constructor->Invoke(param);
  80.     // オープン
  81.     MethodInfo^ method = GetMethod(assem, "RIFF.WaveReader", "Open");
  82.     method->Invoke(reader, nullptr);
  83.     // フォーマットの取得
  84.     PropertyInfo^ prop = GetProperty(assem, "RIFF.WaveReader", "Format");
  85.     Object^ format = prop->GetValue(reader,nullptr);
  86.     FieldInfo^ fi = format->GetType()->GetField("nSamplesPerSec");
  87.     wi.hz = (int)(UInt32)(fi->GetValue(format));
  88.     fi = format->GetType()->GetField("nChannels");
  89.     wi.ch = (int)(UInt16)(fi->GetValue(format));
  90.     // サンプルの総数
  91.     prop = GetProperty(assem, "RIFF.WaveReader", "Samples");
  92.     int samples = (int)(prop->GetValue(reader,nullptr));
  93.      //読み込むサンプル数を計算
  94.     int sc = samples - start;
  95.     if(sc>length)
  96.         sc = length;
  97.     if(sc>0)
  98.     {
  99.         //実際に読み込みを行う
  100.         wi.pcm_length = sc;
  101.         try
  102.         {
  103.             // 読み込みメソッドを取得
  104.             method = GetMethod(assem, "RIFF.WaveReader", "ReadSamples");
  105.             //ここで読み出されるのは、16ビット  チャネル数が単位
  106.             param=gcnew array<Object^>(2);
  107.             param[0] = start;
  108.             param[1] = wi.pcm_length;
  109.             array<short>^ pcm = (array<short>^)(method->Invoke(reader, param));
  110.             wi.pcm_length *= wi.ch;//R言語に返すサンプル数は16ビットが単位
  111.             if(wi.pcm_length>0)
  112.             {
  113.                 wi.pcm = (short*)malloc(wi.pcm_length * 2);
  114.                 IntPtr^ p = gcnew System::IntPtr(wi.pcm);
  115.                 System::Runtime::InteropServices::Marshal::Copy(pcm, 0, *p, wi.pcm_length);
  116.             }
  117.         }
  118.         catch(System::Exception^ e)
  119.         {
  120.             Error(e->Message);
  121.             return wi;
  122.         }
  123.     }
  124.     // クローズ
  125.     method = GetMethod(assem, "RIFF.WaveReader", "Close");
  126.     method->Invoke(reader, nullptr);
  127.     return wi;
  128. }

リフレクションでも型を参照したい

  これは、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クラスの定義)
Wave16R0.cpp Wave16R1.cpp
Rのスクリプトから呼び
出されるCの関数
C#のアセンブリを呼び出す
CLI/C++のクラス
  1. Rの型を使うには、Rのヘッダファイルをインクルードすることになります。
    同時に、一般的なヘッダファイルもインクルードされます。
    using namespace System;
    と、書くと競合が起きてコンパイルやリンクでエラーが起きます。
  2. Wave16R0.cppでRの型を扱うことにすると、CLRの型を避けることになります。
    Wave16R1.cppのCLR部分とのインタフェースには、Cの型のみを使うことにします。
    このインタフェースはWaveIO_Rクラスで行います。
  3. WaveIO_Rクラスは、条件コンパイルで、Wave16R0.cppに使われた場合は、CLRな型を隠します。

  Wave16R0.h

  1. class WavIO_R
  2. {
  3. public:
  4.     struct WAVINFO
  5.     {
  6.         int ch;
  7.         int hz;
  8.         int pcm_length;
  9.         short* pcm;
  10.     };
  11. private:
  12. #ifdef R_CLR
  13.     static void DBG(String^ s)
  14.     {
  15.         TextWriter^ tw = gcnew StreamWriter("a4.txt",true);
  16.         tw->WriteLine(s);
  17.         tw->Close();
  18.     }
  19.     static Assembly^ GetAssembly();
  20.     static ConstructorInfo^ GetConstructor(Assembly^ assem, String^ className, int arg_count, int arg_index, Type^ arg_type);
  21.     static MethodInfo^ GetMethod(Assembly^ assem, String^ className, String^ methodName);
  22.     static MethodInfo^ GetMethod2(Assembly^ assem, String^ className, String^ methodName,
  23.             int nParams, int param_index, Type^ param_type);
  24.     static PropertyInfo^ GetProperty(Assembly^ assem, String^ className, String^ propertyName);
  25. #endif
  26. public:
  27. #ifdef R_CLR
  28.     static void Error(System::String^ s);
  29. #endif
  30. public:
  31.     WavIO_R(void);
  32.     ~WavIO_R(void);
  33.     static void Play(int* pcm, int len, int ch, int hz);
  34.     static void WriteFile(char* path, int* pcm, int len, int ch, int hz);
  35.     static WAVINFO ReadFile(const char* path, int start, int samples);
  36.     static WAVINFO Record(int ch, int hz, int sec);
  37. };

mikeo_410@hotmail.com