COMとdynamicとMarshal.ReleaseComObjectと(2)

前回の結論「dynamicを使ってもCOMの解放まで不要になったわけじゃない」とすると、安全性だけで言えばC#2.0時代に良く使われていたテクニック「COMを使った処理をVBScriptに分離する」のほうが良いということになります。

なので今回は自身の復習を兼ねて、C#では鬼門の"Excel Automation"をC#2.0+VBScriptで。
かつ、ただ裸のVBScriptを外部プログラムとして実行するのは「ソースが丸見え」他の理由でイマイチなので、埋め込みリソース化も合わせて行ってみます。

まずはVBScriptのソース。

Option Explicit
Const xlWorkbookNormal = -4143

'for TEST
Dim args
Set args = WScript.Arguments
If (args.Count > 1) Then
	Main args(0), args(1), True
End If
WScript.Quit()

Sub Main(path, val, visible)
	Dim objExcel, objWorkBook

	Set objExcel = CreateObject("Excel.Application")
	objExcel.Visible = visible

	Set objWorkBook = objExcel.WorkBooks.Add
	objExcel.Range("A1").Value = val

	objExcel.DisplayAlerts = False
	objWorkBook.SaveAs path, xlWorkbookNormal

	objWorkBook.Close
	objExcel.Quit

	Set objWorkBook = nothing
	Set objExcel = nothing
End Sub

やってることは単純で、第1パラメータで指定したファイル名でExcelブック(2003形式)を作成し、第2パラメータで指定した文字列をセルA1に書き込むだけです。たったこれだけの処理でも、もしC#側に実装していたらtry-catchとMarshal.ReleaseComObject()の嵐になることでしょう。
処理の実体はMain()プロシージャですが、あえてコマンドパラメータ解析処理も実装しておくことでこのVBScriptだけでも単体実行できるように作ってありますのでテストが容易です。

これを"script.vbs"という名前でVisualStudioのプロジェクトに登録します。その際にプロパティの「ビルドアクション」を「埋め込まれたリソース」に設定してください。

これを呼び出すのは、MSScriptControlを参照設定した以下のC#プログラムです。

using MSScriptControl;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Resources;
using System.IO;

            const string scriptName = "script.vbs";

            ScriptControl scrControl = null;
            try
            {
                scrControl = new ScriptControl();
                scrControl.Language = "VBScript";
                
                Assembly asm = Assembly.GetExecutingAssembly();
                using (Stream resStream = asm.GetManifestResourceStream(asm.GetName().Name + "." + scriptName))
                {
                    using (StreamReader resReader = new StreamReader(resStream))
                    {
                        try
                        {
                            scrControl.AddCode(resReader.ReadToEnd());
                        }
                        catch (COMException e)
                        {
                            //XXX - WScriptの未定義エラーは無視(ScriptControlからは使えないので)
                            if (!(e.ErrorCode == -2146827788 && e.ToString().Contains("WScript"))) throw e;
                        }
                        scrControl.Run("Main", new object[] {"a.xls", "foo", false});
                    }
                }
            }
            finally
            {
                if (scrControl != null)
                {
                    Marshal.FinalReleaseComObject(scrControl);
                    scrControl = null;
                }
            }

MSScriptControl自体もCOMですが、Excelオブジェクトを直接C#で扱うときとは違い、管理すべきCOMオブジェクトは1個だけで済みますから解放し損ねることは無いでしょう。
さらに念には念を入れて解放にはMarshal.ReleaseComObject()ではなく、あえてMarshal.FinalReleaseComObject()を使っています。これは前回参照した記事中に「どうせ使うならFinalのほうを使え」と書いてあったので。

COMExceptionのcatchで変なことをしていますが、これはMSScriptControl経由で実行したVBScript中では"WScript"オブジェクトが使えないための苦肉の策です。
本来C#から実行するのに必要なのはMain()プロシージャだけなのでWScriptオブジェクトは不要なのですが、上記で書いた通り「VBScript単体テストのためにコマンドパラメータの解析が必要」=「WScriptオブジェクトが必要」なので。
たぶんVBScript側の"Option Explicit"を外すという方法でも回避できると思いますが、それはそれでイマイチかと。

こうやって書いてみると、やっぱり「リソース解放は自前でやらなければならないC#4.0のdynamicでは片手落ち」ですね。もっとお気楽、安全にCOMを使えるようにならないものでしょうか。