IronPython を中継して、C#からPythonライブラリを使ってみる
- IronPython 1.0.1
Python の.NET実装 - pyExcelerator 0.6.3a
EXCEL ファイルを Python から読み書きするライブラリ
IronPython から Python ライブラリを使うことは難しくないということなのでやってみた。
それだけじゃ面白くないので、さらにC#を連携させてみたらなんとか動いたのでメモ。
- サンプル一式
一応 EXCELのない環境でも .NET2.0 があれば動くはず・・・(そんな環境あるのか怪しいけど)
ただし以下のような問題があって、解決方法が思い浮かばない。
- 日本語を含むと「System.Text.EncoderFallbackException」が発生する。ファイル操作自体は問題なさげ。
- pyExcelerator で __slots__ が定義されているクラスをIronPythonに読み込めない。 __slots__ を削除すると読み込める。(サンプルは削除済み)
- セルに書き込めるのは文字列のみ。
- セルに日付が入力されていても判定できない。(pyExcelerator で判定する方法が分からない)
- 初回起動が遅い。
NAnt でアセンブリのバージョンアップデート
via CodeZine:NAntでアセンブリのバージョンアップデートを自動化する
複数アセンブリのバージョン番号管理を自動化する、いい方法はないかと考えている時に上記の記事を見つけた。
アイデア自体はいいんだけど、ビルド番号を決定するスクリプトがどうも気持ち悪い。
なんとかしようとタスクリファレンスを眺めていたら、アセンブリからバージョン番号を取得する関数があるじゃないですか。
現在のバージョン番号はアセンブリに埋め込まれているわけだから、そこから次のバージョン番号を決定すればいいのでは?と考えてやってみたらうまくいったのでメモしておく。
ついでにサンプル一式
続きを読むlog4net でログレベル毎に出力先を指定する
現在のプロジェクトでは、log4net を使用してログを出力することになっていた。
ログファイル名は日付によって変わるが、その日のすべてのログが1つのファイルに出力される。
ある時、ログレベル毎に出力先のファイルを変更できれば便利じゃないか?と指摘された。
確かに、と思っていろいろ調べてみたが、どうも見つけられない。
それならば、と本家のサイトを見てみたらあっさり見つかった。
キーになるのは log4net.Filter.LevelRangeFilter というフィルタクラス。
こいつに出力したいログレベルの範囲を指定して、ログレベル毎にアペンダを設定してやればいい。
DEBUG・INFO・ERROR で出力ファイルを分けてみる。
ここにWindowsアプリのソースを置いておく。
このアプリでは、Form上のボタンを押すとログレベル別にログが出力される。
アプリケーション構成ファイルはこんな感じ。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="System.Configuration.IgnoreSectionHandler" /> </configSections> <log4net> <appender name="DebugAppender" type="log4net.Appender.FileAppender"> <param name="File" value="C:\log\Debug.log" /> <param name="AppendToFile" value="true" /> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="DEBUG" /> <levelMax value="DEBUG" /> </filter> </appender> <appender name="InfoAppender" type="log4net.Appender.FileAppender"> <param name="File" value="C:\log\Info.log" /> <param name="AppendToFile" value="true" /> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="INFO" /> <levelMax value="INFO" /> </filter> </appender> <appender name="ErrorAppender" type="log4net.Appender.FileAppender"> <param name="File" value="C:\log\Error.log" /> <param name="AppendToFile" value="true" /> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="ERROR" /> <levelMax value="ERROR" /> </filter> </appender> <root> <level value="DEBUG" /> <appender-ref ref="DebugAppender" /> <appender-ref ref="InfoAppender" /> <appender-ref ref="ErrorAppender" /> </root> </log4net> </configuration>
S2Dao.NET を使ってみる(3)
の続き。以下のような機能を試してみる。
- バインド変数コメント
- 単純な置き換え
- LIKE を使った場合
- IN 句を使った場合
- BEGIN コメントを使ったSQLファイル
ソース一式をここに置いておく。
DB の変更
EMPテーブルだけだとこれらの機能を使うのが難しいので、DEPTテーブルとPOSTテーブルを追加した。
CREATE TABLE [dbo].[EMP] ( [EMPNO] numeric (10, 0) NOT NULL , [ENAME] nvarchar (50) NOT NULL , [DEPTNUM] numeric (10, 0) , [POSTNUM] numeric (10, 0) , CONSTRAINT [PK_EMP2] PRIMARY KEY CLUSTERED ([EMPNO])) GO CREATE TABLE [dbo].[DEPT] ( [DEPTNUM] numeric (10,0) NOT NULL , [DNAME] nvarchar (50) NOT NULL , CONSTRAINT [PK_DEPT] PRIMARY KEY CLUSTERED ([DEPTNUM])) GO CREATE TABLE [dbo].[POST] ( [POSTNUM] numeric (10,0) NOT NULL , [POSTNAME] nvarchar (50) NOT NULL , CONSTRAINT [PK_POST] PRIMARY KEY CLUSTERED ([POSTNUM])) GO
バインド変数コメントで単純な置き換え
画面のように、社員番号の上限と下限を指定して検索できるようにしてみる。
具体的にはこう。
[Query("empno BETWEEN /*start*/1 AND /*end*/1")] IList SelectByEmpNo(int start, int end);
バインド変数コメント(/*変数名*/リテラル)を記述する際に注意するのはこの3点。
- /*変数名*/ というように間にスペースは入れないこと。
- 変数名はメソッドの変数名と一致させること。
- リテラルは省略してもかまわない。
バインド変数コメントで LIKE を使う
画面のように、名前を指定して検索できるようにしてみる。
具体的にはこう。
[Query("ename LIKE /*ename*/'test'")] IList SelectByEname(string ename);
LIKEを使う場合、ワイルドカードはメソッドの引数に埋め込むこと。
バインド変数コメントで IN句 を使う
画面のように、部署を複数指定して検索できるようにしてみる。
具体的にはこう。ADO.NETではあれだけ面倒だったIN句がこんなに簡単に・・・
[Query("deptnum IN /*depts*/(1,2,3)")] IList SelectByDepts(int[] depts);
IN句の場合、「IN (/*depts*/1)」とは書かないので注意が必要。
SQLファイルで BEGIN コメントを使う
画面のように、今までの検索条件をANDでつないで検索できるようにしてみる。
メソッドはこう。
IList SelectByAll(bool hasRange, int start, int end, string ename, int[] depts, int[] posts);
肝心のSQLファイルはこんな感じ。
SELECT EMPNO, ENAME, DEPTNUM, POSTNUM FROM EMP /*BEGIN*/WHERE /*IF hasRange*/EMPNO BETWEEN /*start*/1 AND /*end*/1/*END*/ /*IF ename != null*/AND ENAME LIKE /*ename*/'TEST'/*END*/ /*IF depts != null*/AND DEPTNUM IN /*depts*/(1,2,3)/*END*/ /*IF posts != null*/AND POSTNUM IN /*posts*/(1,2,3)/*END*/ /*END*/
これも「埋め込まれたリソース」なので、名前空間に注意が必要。
ファイル名は「Daoインターフェイス名+_+メソッド名.sql」としておくこと。
(この場合は「IEmpDao_SelectByAll.sql」となる)
BEGINコメント(/*BEGIN*/ 〜 /*END*/)は内部のIFコメント(/*IF 条件*/ 〜 /*END*/)の条件がすべて false の場合、WHERE句自体をなかったことにしてくれるものらしい。しかも途中のANDを適切に除去してくれるブラボーな仕様だ。
まとめ
最終的な Dao のソースはこんな感じ。
Daoインターフェイスではメソッドのオーバーロードをサポートしていないらしいので、名前が重複しないように気をつける必要がある。
using System; using System.Collections; using System.Text; using Seasar.Dao.Attrs; using S2DaoTestApp.Entity; namespace S2DaoTestApp.Dao { [Bean(typeof(Emp))] public interface IEmpDao { IList GetAllList(); [Query("ORDER BY EMPNO DESC")] Emp GetMax(); int Insert(Emp emp); int Update(Emp emp); int Delete(Emp emp); [Query("empno BETWEEN /*start*/1 AND /*end*/1")] IList SelectByEmpNo(int start, int end); [Query("ename LIKE /*ename*/'test'")] IList SelectByEname(string ename); [Query("deptnum IN /*depts*/(1,2,3)")] IList SelectByDepts(int[] depts); [Query("postnum IN /*posts*/(1,2,3)")] IList SelectByPosts(int[] posts); IList SelectByAll(bool hasRange, int start, int end, string ename, int[] depts, int[] posts); } }
ここまで実装した時点では、明らかに
S2Dao.NETを使ったデータアクセスの実装時間 << 画面周りの実装時間
となった。データアクセス部分をADO.NETのみで実装していたらそこまで差はなかったはず。
BEGINコメントとかはちょっと癖があるかなとは思うけど、慣れるのにそれほど時間はかからないように思う。あとはストアドかなあ。
もうちょっと早く知っておきたかった・・・orz
S2Dao.NET を使ってみる(2)
これの続き。
昨日の状態だと全データが見れるだけなので、もう少しアプリケーションらしくしてみる。
追加機能は以下の3つ。
- データの追加(社員番号の最大値+1のデータを追加)
- データの修正(テーブル上で選択しているレコードを修正)
- データの削除(テーブル上で選択しているレコードを削除)
そのソース一式をここに置いておく。
Dao インターフェイスの修正
まず、社員番号の最大値を取得するメソッドを追加する。さすがに自動生成は無理なので Query 属性を使う。WHEREより後のSQLを書けばいいわけだが、今回特に条件がないので ORDER BY から書き始める。具体的には「Query("ORDER BY EMPNO DESC")」とする。
次に INSERT文、UPDATE文、DELETE文を実行するメソッドを追加する。これは規約に従って、メソッド名をそれぞれInsert、Update、Deleteで始めればいい。
最終的なソースはこんな感じ。
using System; using System.Collections.Generic; using System.Text; using Seasar.Dao.Attrs; namespace S2DaoTestApp { [Bean(typeof(Emp))] public interface IEmpDao { Emp[] GetAllList(); [Query("ORDER BY EMPNO DESC")] Emp GetEmpHavingMaxEmpno(); int Insert(Emp emp); int Update(Emp emp); int Delete(Emp emp); } }
Form の修正
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Seasar.Framework.Container; using Seasar.Framework.Container.Factory; namespace S2DaoTestApp { public partial class Form1 : Form { private IS2Container _container; public Form1() { InitializeComponent(); } private IEmpDao EmpDao { get { if (_container == null) { _container = SingletonS2ContainerFactory.Container; } return (IEmpDao)_container.GetComponent(typeof(IEmpDao)); } } private Emp Current { get { if (empBindingSource.Current == null) { return null; } else { return (Emp)empBindingSource.Current; } } } private void buttonGetAll_Click(object sender, EventArgs e) { try { this.Enabled = false; empBindingSource.DataSource = EmpDao.GetAllList(); } finally { this.Enabled = true; } } private void buttonAppend_Click(object sender, EventArgs e) { try { this.Enabled = false; Emp emp = EmpDao.GetEmpHavingMaxEmpno(); using (AppendForm f = new AppendForm(emp.EmpNo + 1)) { DialogResult r = f.ShowDialog(this); if (r == DialogResult.OK) { empBindingSource.DataSource = EmpDao.GetAllList(); } } } finally { this.Enabled = true; } } private void buttonEdit_Click(object sender, EventArgs e) { try { this.Enabled = false; if (this.Current == null) { MessageBox.Show("編集できません", "", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } Emp emp = this.Current; using (EditForm f = new EditForm(emp)) { DialogResult r = f.ShowDialog(this); if (r == DialogResult.OK) { empBindingSource.DataSource = EmpDao.GetAllList(); } } } finally { this.Enabled = true; } } private void buttonDelete_Click(object sender, EventArgs e) { try { this.Enabled = false; if (this.Current == null) { MessageBox.Show("削除できません", "", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } if (MessageBox.Show("削除しますか?", "", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.No) { return; } Emp emp = this.Current; if (EmpDao.Delete(emp) == 1) { empBindingSource.Remove(emp); } } finally { this.Enabled = true; } } } }
コンテナは Program.cs で初期化するように変更した。
using System; using System.Collections.Generic; using System.Windows.Forms; using Seasar.Framework.Container.Factory; namespace S2DaoTestApp { static class Program { [STAThread] static void Main() { SingletonS2ContainerFactory.Init(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Seasar.Framework.Container; using Seasar.Framework.Container.Factory; namespace S2DaoTestApp { public partial class AppendForm : Form { private IS2Container _container; public AppendForm(int newnumber) { InitializeComponent(); textEmpno.Text = newnumber.ToString(); } public IS2Container S2Container { get { if (_container == null) { _container = SingletonS2ContainerFactory.Container; } return _container; } } private bool IsValid { get { if (textEname.Text.Trim().Length == 0) { return false; } if (textDeptnum.Text.Trim().Length == 0) { return true; } int tmp; if (!int.TryParse(textDeptnum.Text, out tmp)) { return false; } return true; } } private void buttonAppend_Click(object sender, EventArgs e) { this.Enabled = false; Emp emp = new Emp(); emp.EmpNo = int.Parse(textEmpno.Text); emp.Ename = textEname.Text; string dept = textDeptnum.Text.Trim(); if ((dept != null) && (dept.Length > 0)) { emp.DeptNum = int.Parse(dept); } IEmpDao dao = (IEmpDao)this.S2Container.GetComponent(typeof(IEmpDao)); int r = dao.Insert(emp); if (r == 1) { this.DialogResult = DialogResult.OK; } } private void buttonCancel_Click(object sender, EventArgs e) { this.Close(); } private void textDeptnum_TextChanged(object sender, EventArgs e) { buttonAppend.Enabled = this.IsValid; } private void textEname_TextChanged(object sender, EventArgs e) { buttonAppend.Enabled = this.IsValid; } } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Seasar.Framework.Container; using Seasar.Framework.Container.Factory; namespace S2DaoTestApp { public partial class EditForm : Form { private IS2Container _container; private Emp _emp; public EditForm(Emp emp) { InitializeComponent(); _emp = emp; textEmpno.Text = _emp.EmpNo.ToString(); textEname.Text = _emp.Ename; if (!_emp.DeptNum.IsNull) { textDeptnum.Text = _emp.DeptNum.ToString(); } } public IS2Container S2Container { get { if (_container == null) { _container = SingletonS2ContainerFactory.Container; } return _container; } } private bool IsValid { get { if (textEname.Text.Trim().Length == 0) { return false; } if (textDeptnum.Text.Trim().Length == 0) { return true; } int tmp; if (!int.TryParse(textDeptnum.Text, out tmp)) { return false; } return true; } } private void buttonEdit_Click(object sender, EventArgs e) { this.Enabled = false; Emp emp = new Emp(); emp.EmpNo = int.Parse(textEmpno.Text); emp.Ename = textEname.Text; string dept = textDeptnum.Text.Trim(); if ((dept != null) && (dept.Length > 0)) { emp.DeptNum = int.Parse(dept); } IEmpDao dao = (IEmpDao)this.S2Container.GetComponent(typeof(IEmpDao)); int r = dao.Update(emp); if (r == 1) { this.DialogResult = DialogResult.OK; } } private void buttonCancel_Click(object sender, EventArgs e) { this.Close(); } private void textDeptnum_TextChanged(object sender, EventArgs e) { buttonEdit.Enabled = this.IsValid; } private void textEname_TextChanged(object sender, EventArgs e) { buttonEdit.Enabled = this.IsValid; } } }
ここまで来てもSQLはほとんど書いてない。やっぱりやばいですな。
S2Dao.NET を使ってみる
開発環境はこんな感じ
- VS2005 Team Edition for Software Developers(C#)
- SqlServer 2005 Developer
- S2Container.NET 1.2.2
- S2Dao.NET 0.4.5
ソース一式をここに置いておく。
DB作成
使用するDBはこんな感じ
CREATE DATABASE [s2daotest] GO use [s2daotest] GO CREATE TABLE [dbo].[EMP] ( [EMPNO] numeric (10, 0) NOT NULL , [ENAME] nvarchar (50) NOT NULL , [DEPTNUM] numeric (10, 0) , CONSTRAINT [PK_EMP] PRIMARY KEY CLUSTERED ([EMPNO])) GO
エンティティの作成
ファイル名は「Emp.cs」で、ほとんど S2Dao.NET に含まれるサンプル(Employee)と同じ。違うのはこれくらいかな。
- DEPTNUM に NULL を許可するために System.Data.SqlTypes.SqlDecimal を使用
- Table属性を省略するためにクラス名を Emp に変更
using System; using System.Collections.Generic; using System.Text; using System.Data.SqlTypes; namespace S2DaoTestApp { public class Emp { private int _empno; public int EmpNo { get { return _empno; } set { _empno = value; } } private string _ename; public string Ename { get { return _ename; } set { _ename = value; } } private SqlDecimal _deptNum; public SqlDecimal DeptNum { get { return _deptNum; } set { _deptNum = value; } } } }
Dao インターフェイスの作成
ファイル名は「IEmpDao.cs」で、EMP テーブルのデータを全件取得するメソッドを定義する。
using System; using System.Collections.Generic; using System.Text; using Seasar.Dao.Attrs; namespace S2DaoTestApp { [Bean(typeof(Emp))] public interface IEmpDao { Emp[] GetAllList(); } }
Ex.dicon ファイル
サンプルからコピーしてきて、プロパティのビルドアクションを「埋め込まれたリソース」にしただけ。ただし、ここを参考に接続文字列を app.config から取得するように変更している。
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN" "http://www.seasar.org/dtd/components21.dtd"> <components namespace="Ex"> <!-- データプロバイダ --> <component name="SqlClient" class="Seasar.Extension.ADO.DataProvider"> <property name="ConnectionType">"System.Data.SqlClient.SqlConnection"</property> <property name="CommandType">"System.Data.SqlClient.SqlCommand"</property> <property name="ParameterType">"System.Data.SqlClient.SqlParameter"</property> <property name="DataAdapterType">"System.Data.SqlClient.SqlDataAdapter"</property> </component> <!-- データソース --> <component name="SqlDataSource" class="Seasar.Extension.Tx.Impl.TxDataSource"> <property name="DataProvider">Ex.SqlClient</property> <property name="ConnectionString">appSettings['ConnectionString']</property> </component> <!-- S2Dao.NETのDaoInterceptorとそれに必要なコンポーネント --> <component class="Seasar.Extension.ADO.Impl.BasicDataReaderFactory" /> <component class="Seasar.Extension.ADO.Impl.BasicCommandFactory" /> <component class="Seasar.Dao.Impl.DaoMetaDataFactoryImpl" /> <component name="DaoInterceptor" class="Seasar.Dao.Interceptors.S2DaoInterceptor"/> <!-- ローカルトランザクション用のインターセプター --> <component name="LocalRequiredTx" class="Seasar.Extension.Tx.TransactionInterceptor"> <arg><component class="Seasar.Extension.Tx.Impl.LocalRequiredTxHandler" /></arg> <property name="TransactionStateHandler">TransactionContext</property> </component> <!-- ローカルトランザクション用のインターセプターで使用します --> <component name="TransactionContext" class="Seasar.Extension.Tx.Impl.TransactionContext"> <property name="IsolationLevel">System.Data.IsolationLevel.ReadCommitted</property> </component> <!-- MSDTC(分散トランザクション)用のインターセプター --> <component name="RequiredTx" class="Seasar.Extension.Tx.TransactionInterceptor"> <arg><component class="Seasar.Extension.Tx.Impl.DTCRequiredTxHandler" /></arg> <property name="TransactionStateHandler">DTCTransactionStateHandler</property> </component> <!-- MSDTC用のインターセプターで使用します --> <component name="DTCTransactionStateHandler" class="Seasar.Extension.Tx.Impl.DTCTransactionStateHandler" /> </components>
app.dicon ファイル
app.dicon ファイルはこんな感じ。これもビルドアクションを「埋め込まれたリソース」にしておく。C#の場合、リソースを埋め込むとデフォルトの名前空間が追加されるので、Ex.dicon をインクルードする場合は「S2DaoTestApp.Ex.dicon」というように指定しないといけない。
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN" "http://www.seasar.org/dtd/components21.dtd"> <components> <include path="S2DaoTestApp.Ex.dicon" /> <component class="S2DaoTestApp.IEmpDao"> <aspect>Ex.DaoInterceptor</aspect> </component> </components>
アプリケーション構成ファイル
app.config はこんな感じ。
Ex.dicon の時と同じように、configPath は「S2DaoTestApp.app.dicon」とする。
エンティティやDaoを見つけるために、assemblys で S2DaoTestApp を忘れずに参照しておくこと。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="seasar" type="Seasar.Framework.Xml.S2SectionHandler, Seasar" /> </configSections> <appSettings> <add key="ConnectionString" value="Server=(local);database=s2daotest;Integrated Security=SSPI" /> </appSettings> <seasar> <configPath>S2DaoTestApp.app.dicon</configPath> <assemblys> <assembly>Seasar.Dao</assembly> <assembly>S2DaoTestApp</assembly> </assemblys> </seasar> </configuration>
動作確認
Form1 に Button と DataGridView を追加して、Button がクリックされたら DataGridView に Emp テーブルの内容が表示されるようにしてみた。
DataGridView にはデータソースとして、Emp クラスを選択しておく。
Form1 のコードはこんな感じ。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Seasar.Framework.Container; using Seasar.Framework.Container.Factory; namespace S2DaoTestApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); SingletonS2ContainerFactory.Init(); } private void button1_Click(object sender, EventArgs e) { try { button1.Enabled = false; IS2Container c = SingletonS2ContainerFactory.Container; IEmpDao dao = (IEmpDao)c.GetComponent(typeof(IEmpDao)); empBindingSource.DataSource = dao.GetAllList(); } finally { button1.Enabled = true; } } } }
最終的なソリューションの構成はこんな感じになった。
適当に EMP テーブルにデータを追加しておくとこんな感じに表示される。
SQLや ADO.NET のコードをまったく書いてないのに、ここまで出来るってやばいですな。
DataSet では扱えない、NULLを許可するフィールドも使えるし。
DllImportを使わずにアンマネージドDLL関数を呼び出してみる
まったく使わないわけではないけど。参考にしたのはこのあたり。
- http://dobon.net/vb/dotnet/links/extractarchive.html
- http://momotchi.net/forums/711/ShowPost.aspx
- http://msdn2.microsoft.com/ja-jp/library/system.runtime.interopservices.marshal.getdelegateforfunctionpointer.aspx
同じようなインターフェイスを持つ、複数のアンマネージドDLL関数を呼び出す必要があったのでいろいろ調べていたら、 .NET 2.0 から用意された System.Runtime.InteropServices.Marshal.GetDelegateForFunctionPointer を使えば関数ポインタをデリゲートに変換できることがわかった。
やり方としてはこんな感じ。
- アンマネージドDLLの関数定義を調べる。
- 関数定義にあわせてデリゲートを定義する。
- LoadLibrary でアンマネージドDLLをロードする。
- GetProcAddress で関数ポインタを取得する。
- GetDelegateForFunctionPointer で関数ポインタをデリゲートに変換する。
- デリゲートをコールする。
- FreeLibrary でアンマネージドDLLを解放する。
で、毎回同じようなコードになる部分をクラスにしてみたのでメモ。
サンプルでは MessageBoxW を呼び出してるけど、統合アーカイバとかにも応用できるはず。
using System; using System.Runtime.InteropServices; namespace LateBindingApplication { static class Program { /// <summary> /// アプリケーションのメイン エントリ ポイントです。 /// </summary> [STAThread] static void Main() { using (LateBinding b = new LateBinding("user32.dll")) { MessageBox m = (MessageBox)b.GetDelegate("MessageBoxW", typeof(MessageBox)); m(IntPtr.Zero, "call by c#", "LateBindingApplication", 0); } } } public delegate int MessageBox(IntPtr hwnd, [MarshalAs(UnmanagedType.LPWStr)]string text, [MarshalAs(UnmanagedType.LPWStr)]string Caption, int type); public class LateBinding : IDisposable { [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPWStr)] string lpFileName); [DllImport("kernel32", SetLastError = true)] private static extern bool FreeLibrary(IntPtr hModule); [DllImport("kernel32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = false)] private static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName); private IntPtr _module; /// <summary> /// DLL名を指定して遅延バインディングを行うオブジェクトを生成します。 /// </summary> /// <param name="filename">バインドするDLL名</param> public LateBinding(string filename) { _module = LateBinding.LoadLibrary(filename); if (_module != IntPtr.Zero) { return; } int result = Marshal.GetHRForLastWin32Error(); throw Marshal.GetExceptionForHR(result); } /// <summary> /// 指定した名前を持つアンマネージ関数ポインタをデリゲートに変換します。 /// </summary> /// <param name="procName">アンマネージ関数名</param> /// <param name="delegateType">変換するデリゲートのType</param> /// <returns>変換したデリゲート</returns> public Delegate GetDelegate(string procName, Type delegateType) { IntPtr ptr = LateBinding.GetProcAddress(_module, procName); if (ptr != IntPtr.Zero) { Delegate d = Marshal.GetDelegateForFunctionPointer(ptr, delegateType); return d; } int result = Marshal.GetHRForLastWin32Error(); throw Marshal.GetExceptionForHR(result); } #region IDisposable メンバ public void Dispose() { if (_module != IntPtr.Zero) { LateBinding.FreeLibrary(_module); } } #endregion } }