動的コンパイルの深淵:CodeDomProviderで実現する「計算式エンジン」の極意
業務アプリケーションの現場において、ハードコーディングされたロジックは「負債」の始まりである。ユーザーから「この計算式を明日から変えたい」「条件分岐を自由に追加したい」と要望されたとき、あなたは再コンパイルと再配布という儀式にどれほどの工数を費やしているだろうか。
今回は、VB.NETのアーキテクチャの根幹をなす`CodeDomProvider`を使い、アプリケーション内で動的にコードを生成・コンパイルし、メモリを極限まで制御しながら実行する「真のスクリプトエンジン」の構築手法を伝授する。
なぜ「インタープリタ」ではなく「コンパイル」なのか
世の中には正規表現でパースする簡易計算機も溢れているが、あれは計算量が増えるほどパフォーマンスが線形(あるいはそれ以上に)悪化する。業務システムで扱う数千行のデータセットに対し、文字列パースを繰り返すのは愚の骨頂だ。
我々が求めるのは機械語への直結である。`CodeDomProvider`を用いることで、実行時に生成したコードをメモリ上のアセンブリとしてロードし、直接呼び出すことが可能になる。これにより、ネイティブコードと遜色ない実行速度を叩き出すことができる。
実装の核心:CodeDomProviderによる動的実行
まずは、ユーザーが入力した数式をラップし、コンパイルしてデリゲートとして呼び出すための最小構成を示す。
Imports System.CodeDom.Compiler
Imports System.Reflection
Imports Microsoft.VisualBasic
Public Class DynamicEngine
‘ コンパイルされたアセンブリのインスタンスをキャッシュする(メモリリーク対策)
Private Shared _cachedMethod As Func(Of Double, Double)
Public Shared Function ExecuteFormula(expression As String, input As Double) As Double
‘ 既にコンパイル済みなら再利用する(パフォーマンスの要)
If _cachedMethod Is Nothing Then
Dim code As String = $@”
Public Class FormulaContainer
Public Function Calculate(x As Double) As Double
Return {expression}
End Function
End Class”
Dim provider As New VBCodeProvider()
Dim parameters As New CompilerParameters()
‘ メモリ内のみで完結させ、ディスクI/Oを発生させない
parameters.GenerateInMemory = True
parameters.ReferencedAssemblies.Add(“System.dll”)
Dim results As CompilerResults = provider.CompileAssemblyFromSource(parameters, code)
If results.Errors.HasErrors Then
Throw New Exception(“コンパイルエラー: ” & results.Errors(0).ErrorText)
End If
‘ リフレクションによるメソッドの抽出
Dim assembly As Assembly = results.CompiledAssembly
Dim type As Type = assembly.GetType(“FormulaContainer”)
Dim method As MethodInfo = type.GetMethod(“Calculate”)
_cachedMethod = Function(val) CDbl(method.Invoke(Activator.CreateInstance(type), New Object() {val}))
End If
Return _cachedMethod(input)
End Function
End Class
シニアエンジニアが意識すべき「闇」の最適化
この手法には、レガシー環境で生き抜いてきた者なら誰もが嗅ぎつける「特有の落とし穴」が存在する。
1. AppDomainの断片化とメモリリーク
`CodeDomProvider`は、デフォルトでは新しい`Assembly`をロードするたびに、一度ロードされたらアンロードできない「ドメイン」に蓄積する。これを無策で行えば、数千回の再コンパイルでアプリケーションはメモリを喰い尽くしてクラッシュする。
- 解法: `AppDomain`を分離して利用し、計算が終わるごとにそのドメインを破棄する手法が正攻法だが、パフォーマンスとのトレードオフになる。頻繁に式が変わらないのであれば、上記のコードのようにキャッシュ戦略(Singleton)を徹底せよ。
2. ユーザー入力という名の「爆弾」
`Return {expression}` の部分に `System.IO.File.Delete(“C:\Windows\System32\…”)` のような悪意あるコードを注入されるリスクがある。
- 解法: コンパイル前に`Regex`で禁止ワードをフィルタリングするか、`PermissionSet`を使用して、特定の名前空間(`System.IO`や`System.Reflection`)へのアクセスを制限したセキュリティコンテキストで実行させるのが、伝説的エンジニアの嗜みだ。
3. Windows APIとの連携
もしスクリプト内で「特定のDLL内の関数を呼び出したい」という要望があった場合、`CompilerParameters.ReferencedAssemblies`に物理パスを追加すれば、外部のネイティブDLLすら呼び出せるようになる。レガシーなVB6時代のDLLを、最新のVB.NETから動的にラップして呼び出す。これぞ最強のレガシー保守術である。
結びに代えて
動的コンパイルは諸刃の剣だ。しかし、この「アプリケーションが自己のコードを構築・拡張する」というアーキテクチャを身につければ、仕様変更のたびにソースコードを修正し、再コンパイルして顧客の環境へデプロイするという、消耗しきったワークフローから解放される。
コードは書くものではない。コードを生み出す「仕組み」を設計することこそが、我々アーキテクトの真の仕事である。
さあ、あなたのアプリケーションに「無限の拡張性」という名の魂を吹き込んでくれ。健闘を祈る。
