自作Cライブラリ内関数の呼び出し
自作のCライブラリ内の関数を呼び出し、文字列を渡す方法について解説します。
呼び出し元コード(C#)として「PInvokeExample2.cs」、呼び出し先コード(C)として「pinvoke.c」(「libpinvoke.so」としてビルド)を使用します。
using System; using System.Runtime.InteropServices; using System.Text; namespace PInvokeExample { [StructLayout(LayoutKind.Sequential)] public struct MyStr { [MarshalAs(UnmanagedType.ByValTStr, SizeConst=51)] public string buffer; public int size; } public class Example2 { [DllImport("libpinvoke")] static extern void sub_str1(string istr); [DllImport("libpinvoke")] static extern void sub_str2(byte[] iostr); [DllImport("libpinvoke")] static extern void sub_str3(string istr, StringBuilder ostr); [DllImport("libpinvoke")] static extern void sub_str4(ref MyStr iostr); [DllImport("libpinvoke")] static extern IntPtr fnc_str5(string istr); [DllImport("libpinvoke")] static extern IntPtr fnc_str6(string istr); [DllImport("libpinvoke", EntryPoint="fnc_str6")] static extern string fnc_str7(string istr); public static void Main(string[] args) { if (args.Length != 1) { Console.WriteLine("Usage: PInvokeExample2.exe [文字列]"); return; } Encoding utf8Enc = Encoding.UTF8; // パターン1 Console.WriteLine("Calling sub_str1..."); Console.WriteLine("[managed ] Send string is {0}.", args[0]); sub_str1(args[0]); // パターン2 byte[] iostr = utf8Enc.GetBytes(args[0]); Array.Resize(ref iostr, 51); Console.WriteLine("\nCalling sub_str2..."); Console.WriteLine("[managed ] Send string is {0}.", utf8Enc.GetString(iostr)); sub_str2(iostr); Console.WriteLine("[managed ] Received string is {0}.", utf8Enc.GetString(iostr)); // パターン3 StringBuilder ostr = new StringBuilder(51); Console.WriteLine("\nCalling sub_str3..."); Console.WriteLine("[managed ] Send string is {0}.", args[0]); sub_str3(args[0], ostr); Console.WriteLine("[managed ] Received string is {0}.", ostr); // パターン4 MyStr mstr; mstr.buffer = args[0]; mstr.size = utf8Enc.GetByteCount(args[0]); Console.WriteLine("\nCalling sub_str4..."); Console.WriteLine("[managed ] Send string/size is {0}/{1}.", mstr.buffer, mstr.size); sub_str4(ref mstr); Console.WriteLine("[managed ] Received string/size is {0}/{1}.", mstr.buffer, mstr.size); // パターン5 Console.WriteLine("\nCalling fnc_str5..."); Console.WriteLine("[managed ] Send string is {0}.", args[0]); Console.WriteLine("[managed ] Received string is {0}.", Marshal.PtrToStringAuto(fnc_str5(args[0]))); // パターン6 Console.WriteLine("\nCalling fnc_str6..."); Console.WriteLine("[managed ] Send string is {0}.", args[0]); IntPtr rptr = fnc_str6(args[0]); Console.WriteLine("[managed ] Received string is {0}.", Marshal.PtrToStringAuto(rptr)); Marshal.FreeHGlobal(rptr); // パターン7 Console.WriteLine("\nCalling fnc_str7..."); Console.WriteLine("[managed ] Send string is {0}.", args[0]); string rstr = fnc_str7(args[0]); Console.WriteLine("[managed ] Received string is {0}.", rstr); } } } /* * ビルド: * * gmcs PInvokeExample2.cs * * 実行: * * mono PInvokeExample2.exe [文字列] * */
#include <stdio.h> #include <string.h> void sub_str1( const char* istr ); void sub_str2( char iostr[51] ); void sub_str3( const char* istr, char ostr[51] ); typedef struct mystr { char buffer[51]; int size; }MYSTR; void sub_str4( MYSTR* iostr ); char* fnc_str5( const char* istr ); char* fnc_str6( const char* istr ); void sub_str1(const char* istr) { printf("[unmanaged] Received string is %s.\n", istr); } void sub_str2(char iostr[51]) { char wstr[51] = "Hello "; size_t len; printf("[unmanaged] Received string is %s.\n", iostr); len = strlen(iostr); if (len <= 44) { strcat(wstr, iostr); } else { strncat(wstr, iostr, 44); } strcpy(iostr, wstr); printf("[unmanaged] Send string is %s.\n", iostr); } void sub_str3(const char* istr, char ostr[51]) { char wstr[51] = "Hello "; size_t len; printf("[unmanaged] Received string is %s.\n", istr); len = strlen(istr); if (len <= 44) { strcat(wstr, istr); } else { strncat(wstr, istr, 44); } strcpy(ostr, wstr); printf("[unmanaged] Send string is %s.\n", ostr); } void sub_str4(MYSTR* iostr) { char wstr[51] = "Hello "; printf("[unmanaged] Received string/size is %s/%d.\n", iostr->buffer, iostr->size); if (iostr->size <= 44) { strcat(wstr, iostr->buffer); } else { strncat(wstr, iostr->buffer, 44); } strcpy(iostr->buffer, wstr); iostr->size = strlen(wstr); printf("[unmanaged] Send string/size is %s/%d.\n", iostr->buffer, iostr->size); } char* fnc_str5(const char* istr) { static char ostr[51] = "Hello "; size_t len; printf("[unmanaged] Received string is %s.\n", istr); len = strlen(istr); if (len <= 44) { strcat(ostr, istr); } else { strncat(ostr, istr, 44); } printf("[unmanaged] Send string is %s.\n", ostr); return ostr; } char* fnc_str6(const char* istr) { char* ostr = NULL; size_t len; printf("[unmanaged] Received string is %s.\n", istr); len = strlen(istr); ostr = (char*)malloc(sizeof(char) * (len + 7)); if (ostr == NULL) { printf("[unmanaged] malloc failed.\n"); } else { strcpy(ostr, "Hello "); strcat(ostr, istr); printf("[unmanaged] Send string is %s.\n", ostr); } return ostr; }
作業する上でのディレクトリ構造は、下表の通りとします。
C#コード格納先 | ~/src/cs/PInvokeExample |
Cコード格納先 | ~/src/c |
自作ライブラリ格納先 | ~/lib |
作業用シェルスクリプト格納先 | ~/bin(環境変数「PATH」に設定済み) |
また、~/.profile等で環境変数「LD_LIBRARY_PATH」に自作ライブラリ格納先を設定します。
if test -z "$LD_LIBRARY_PATH" ; then export LD_LIBRARY_PATH="$HOME/lib" else export LD_LIBRARY_PATH="$HOME/lib:$LD_LIBRARY_PATH" fi
「pinvoke.c」のビルドに次のようなシェルスクリプト「c2so.sh」を使用しました。「~/bin」に保存し、実行権限を与えておきます。
#!/bin/sh USERLIB="$HOME/lib" USERSRC="$HOME/src/c" exec gcc -fPIC -shared -O -o $USERLIB/lib$1.so $USERSRC/$1.c
> c2so.sh pinvoke
> cd ~/src/cs/PInvokeExample > gmcs PInvokeExample2.cs
以下、今回テストしたパターンについて解説します。
ただ文字列を渡してみる
パターン1は、呼び出し元から呼び出し先へ文字列を渡すだけです。呼び出し元と呼び出し先の対応は以下になります。
[DllImport("libpinvoke")] static extern void sub_str1(string istr);
void sub_str1( const char* istr );
呼び出し元で「string」型(参照型)の引数を「値渡し」しているので、方向属性は[In]となり、呼び出し先での引数の内容の変更は行えません(反映されません)。 呼び出し先の引数は「char*」でも動作可能ですが、変更不可ということで「const」指定が安全だと思います。方向属性については『方向属性』(msdn)を参照して下さい。
呼び出し元でのプロトタイプ宣言をより詳細に記述すると、以下の様になります。
[DllImport("libpinvoke", CharSet=CharSet.Ansi)] static extern void sub_str1([In] string istr);
> mono PInvokeExample2.exe testテスト Calling sub_str1... [managed ] Send string is testテスト. [unmanaged] Received string is testテスト. ...
文字列を渡して、編集された文字列を受け取ってみる
パターン2は、呼び出し先で受け取った文字列を編集して引数の内容を変更します。呼び出し元でプロトタイプ宣言したメソッド(関数)の引数の型として「byte[]」を使用しています。
[DllImport("libpinvoke")] static extern void sub_str2(byte[] iostr);
void sub_str2( char iostr[51] );
当初、呼び出し元の引数の型として「StringBuilder」型を試してみましたが、呼び出し先で変更した内容が呼び出し元で反映されなかったり、日本語文字列を渡すと呼び出し先に正しく渡らなかったり(文字化け状態での表示)など、なかなかうまくいきません。そこで引数の型として「byte[]」を使用しました。
「StringBuilder」型を引数として使用した場合、方向属性は[In/Out]になりますが、「byte」型の配列ではどうなるのか、無指定で問題ないので、[In/Out]ということになりますが、あやふやな場合は[In/Out]指定をしたほうが無難だと思います。
[DllImport("libpinvoke")] static extern void sub_str2([In, Out] byte[] iostr);
また、パターン3のように、入力と出力で引数を分ける形で「StringBuilder」型を試してみると、問題は発生しませんでした。
[DllImport("libpinvoke")] static extern void sub_str3(string istr, StringBuilder ostr);
void sub_str3( const char* istr, char ostr[51] );
> mono PInvokeExample2.exe testテスト ... Calling sub_str2... [managed ] Send string is testテスト. [unmanaged] Received string is testテスト. [unmanaged] Send string is Hello testテスト. [managed ] Received string is Hello testテスト. Calling sub_str3... [managed ] Send string is testテスト. [unmanaged] Received string is testテスト. [unmanaged] Send string is Hello testテスト. [managed ] Received string is Hello testテスト. ...
構造体を渡してみる
パターン4では、構造体に固定長文字列を組み込む形で、構造体として渡します。
[StructLayout(LayoutKind.Sequential)] public struct MyStr { [MarshalAs(UnmanagedType.ByValTStr, SizeConst=51)] public string buffer; public int size; } ... [DllImport("libpinvoke")] static extern void sub_str4(ref MyStr iostr);
typedef struct mystr { char buffer[51]; int size; }MYSTR; void sub_str4( MYSTR* iostr );
呼び出し元での構造体の定義では、StructLayoutAttribute属性で構造体の各メンバーのメモリ内での配置の制御を、MarshalAsAttribute属性で呼び出し先との対応(固定長文字配列として)を設定しています。
呼び出し元の引数にパラメータ修飾子「ref」をつけることで、構造体(値型)の「参照渡し」を行うことになります。この指定により、引数の方向属性は「In/Out」となり、引数の内容の変更が呼び出し先で可能になります。
> mono PInvokeExample2.exe testテスト ... Calling sub_str4... [managed ] Send string/size is testテスト/13. [unmanaged] Received string/size is testテスト/13. [unmanaged] Send string/size is Hello testテスト/19. [managed ] Received string/size is Hello testテスト/19. ...
文字列を渡して、編集された文字列を戻り値として受け取ってみる
パターン5では、呼び出し元のプロトタイプ宣言で、戻り値の型として「IntPtr」型を使用しています。呼び出し先で返しているのは、修飾子「static」を付けた文字配列(の先頭アドレス)で、この場合、呼び出し元では「string」型で受け取ることはできません。
パターン6では、呼び出し先で動的に割り当てたメモリ領域を「char」型のポインタとして返し、呼び出し元で「IntPtr」型で受け取り、手動で、受け取ったポインタの開放を行っています。また、パターン6と同じ関数を呼び出して、戻り値を「string」型として受け取ることが可能です(パターン7)。
[DllImport("libpinvoke")] static extern IntPtr fnc_str5(string istr); [DllImport("libpinvoke")] static extern IntPtr fnc_str6(string istr); [DllImport("libpinvoke", EntryPoint="fnc_str6")] static extern string fnc_str7(string istr);
char* fnc_str5( const char* ); char* fnc_str6( const char* );
> mono PInvokeExample2.exe testテスト ... Calling fnc_str5... [managed ] Send string is testテスト. [unmanaged] Received string is testテスト. [unmanaged] Send string is Hello testテスト. [managed ] Received string is Hello testテスト. Calling fnc_str6... [managed ] Send string is testテスト. [unmanaged] Received string is testテスト. [unmanaged] Send string is Hello testテスト. [managed ] Received string is Hello testテスト. Calling fnc_str7... [managed ] Send string is testテスト. [unmanaged] Received string is testテスト. [unmanaged] Send string is Hello testテスト. [managed ] Received string is Hello testテスト.