破滅的なメモリ枯渇を回避せよ:大量図面一括変換における「プロセスの断続的再起動」アーキテクチャ
数千、あるいは数万という単位のDWGファイルを、バージョン変換(例:2018形式から2013形式へのダウングレード)や一括属性更新のためにループ処理する。これは、業務自動化における「日常」でありながら、同時に「最も危険な戦場」でもある。
多くのエンジニアは、単純な `For Each` ループの中に `Documents.Open` と `SaveAs` を詰め込み、`Set obj = Nothing` を記述して「完璧な解放」を謳う。しかし、彼らは決定的な真実を見落としている。AutoCADという巨大なC++アプリケーションが、COMインターフェースを通じてメモリを消費し続けるプロセスにおいて、VBAのガベージコレクション(参照カウントのゼロ化)だけでは、ホストプロセスのメモリ肥大化(メモリリーク)を止めることは不可能だ。
本稿では、単なるコードの書き方ではなく、プロセスレベルでの生存戦略——「Controller(制御側)」と「Worker(実行側)」を分離し、一定数ごとにAutoCADのプロセスそのものを物理的に破棄・再起動する、極限の堅牢性を備えたアーキテクチャを解説する。
1. 宿命的な問題:なぜ `Set obj = Nothing` では足りないのか
VBAにおけるオブジェクト解放は、あくまで「COM参照カウント」を減らす行為に過ぎない。AutoCADの内部エンジン(C++層)は、図面を開くたびに以下のリソースを蓄積していく。
- Undo Stack(元に戻す履歴)のメモリ占有
- 辞書(Dictionaries)やシンボルテーブルのキャッシュ
- フォント、画層、スタイル情報の内部レジストリ
- COMインターフェースのオーバーヘッドによるヒープ領域の断片化
これらは、VBA側でオブジェクトを `Nothing` にしたとしても、AutoCADのプロセス(`acad.exe`)が生存している限り、OSから見れば「使用中」として扱われ続け、やがて仮想メモリを食いつぶし、システム全体のパフォーマンスを著しく低下させるか、最悪の場合 `Out of Memory` でクラッシュを引き起こす。
大量処理における正解は、「プロセスを使い捨てにする」ことだ。
2. 解決策:Controller-Worker モデルの構築
単一のVBAマクロで全てを完結させてはならない。以下の二層構造を構築する。
1. Controller (外部制御層): VBScript またはスタンドアロンの実行ファイル。図面リストを管理し、AutoCADのプロセスを起動・監視し、一定数処理が終わるごとにプロセスを強制終了(Kill)して再起動する。
2. Worker (AutoCAD内部層): AutoCAD内で実行されるVBA。Controllerから渡された「特定のバッチ分」の図面のみを処理し、完了したら自ら終了(またはControllerに制御を戻す)する。
これにより、メモリリークの源泉である `acad.exe` を定期的にリフレッシュし、常にクリーンなメモリ状態で処理を継続できる。
3. 実装:極限の堅牢性を備えたコード・アーキテクチャ
ここでは、最も導入が容易でありながら強力な VBScriptによるController と、AutoCAD VBAによるWorker の実装例を示す。
A. Controller: VBScriptによるプロセス・オーケストレーション
このスクリプトは、AutoCADを「道具」として扱い、その寿命を完全に支配する。
' =============================================================================
' Controller: DWG Batch Processor Orchestrator
' 機能: 図面リストを分割し、AutoCADプロセスを一定数ごとに再起動する
' =============================================================================
Option Explicit
Dim objFSO, objFile, strFileList, strCurrentFile
Dim intBatchSize, intProcessedCount, intTotalFiles
Dim objShell, strAcadPath, strCommand
' --- 設定領域 ---
strFileList = "C:\Work\DWG_List.txt" ' 処理対象のフルパスが並んだテキストファイル
intBatchSize = 50 ' 何枚ごとにAutoCADを再起動するか(メモリ状況に応じて調整)
strAcadPath = "C:\Program Files\Autodesk\AutoCAD 20xx\acad.exe"
' ----------------
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")
If Not objFSO.FileExists(strFileList) Then
WScript.Echo "Error: File list not found."
WScript.Quit
End If
' ファイルリストの読み込みと総数のカウント
Dim arrFiles()
Dim count : count = 0
Set objFile = objFSO.OpenTextFile(strFileList, 1)
Do Until objFile.AtEndOfStream
count = count + 1
ReDim Preserve arrFiles(count - 1)
arrFiles(count - 1) = objFile.ReadLine
Loop
objFile.Close
intTotalFiles = count
intProcessedCount = 0
WScript.Echo "Starting Batch Process. Total: " & intTotalFiles & " files."
' --- メインループ ---
Do While intProcessedCount < intTotalFiles
' 新しいAutoCADインスタンスの起動
' /b はバックグラウンド実行、/c はコマンドライン引数(ここではWorkerを起動するスクリプトを指定)
' ※実際には、Worker用のVBAを起動する仕組みが必要
WScript.Echo "Launching new AutoCAD instance for batch starting at " & (intProcessedCount + 1)
' AutoCADを起動し、Workerマクロを実行するコマンドを構築
' ここでは、Workerに「開始インデックス」と「終了インデックス」を渡す設計とする
Dim startIdx, endIdx
startIdx = intProcessedCount
endIdx = intProcessedCount + intBatchSize - 1
If endIdx >= intTotalFiles Then endIdx = intTotalFiles - 1
' AutoCADを起動(コマンドライン引数を利用してVBAをトリガー)
' 注意: AutoCADの仕様により、直接VBA引数を渡すには特定のスイッチや
' 事前に作成した起動用スクリプトが必要。ここでは概念的な実行を示す。
strCommand = """" & strAcadPath & """ /p ""C:\Work\WorkerLauncher.scr"""
objShell.Run strCommand, 1, True ' Trueで終了を待機
' 進捗更新
intProcessedCount = endIdx + 1
' プロセスが残っている場合の強制終了(ゾンビプロセス対策)
' Windows APIに近い挙動をShell経由で実行
objShell.Run "taskkill /F /IM acad.exe /T", 0, True
Next
WScript.Echo "All tasks completed successfully."
' =============================================================================
' 補足: WorkerLauncher.scr (AutoCADスクリプトファイル)
' 内容例:
' (command "_load" "C:/Work/WorkerModule.dvb")
' (command "_RunBatch" "0" "50")
' =============================================================================
B. Worker: AutoCAD VBAによる高効率処理
Workerは、渡された範囲の図面を処理することだけに集中する。
' =============================================================================
' Worker: AutoCAD VBA Module
' 機能: 指定された範囲の図面を確実に処理し、エラーをログに記録する
' =============================================================================
Option Explicit
' 外部からインデックスを受け取るためのグローバル変数
Public g_StartIndex As Long
Public g_EndIndex As Long
Sub RunBatch()
' ※実際には、スクリプトや外部から引数を渡す仕組み(Commandline等)を利用
' ここではデモとして固定値を代入
g_StartIndex = 0
g_EndIndex = 49
ExecuteProcessing
End Sub
Sub ExecuteProcessing()
Dim i As Long
Dim doc As AcadDocument
Dim filePath As String
Dim logFile As String
Dim fso As Object
logFile = "C:\Work\process_log.txt"
Set fso = CreateObject("Scripting.FileSystemObject")
' 図面リストの読み込み(Controllerと同じリストを使用)
Dim arrFiles As Variant
arrFiles = LoadFileList("C:\Work\DWG_List.txt")
On Error Resume Next
For i = g_StartIndex To g_EndIndex
If i >= UBound(arrFiles) Then Exit For
filePath = arrFiles(i)
' 1. 図面のオープン
Set doc = Documents.Open(filePath)
If Err.Number <> 0 Then
WriteLog logFile, "ERROR: Cannot open " & filePath & " - " & Err.Description
Err.Clear
Else
' 2. 変換・更新ロジック(ここが心臓部)
PerformTransformation doc
' 3. 保存とクローズ
' SaveAs を使用してバージョン指定を行う
' acR2013Drawing は定数。環境に合わせて選択。
doc.SaveAs filePath, acR2013Drawing
doc.Close True
WriteLog logFile, "SUCCESS: " & filePath
End If
' 4. オブジェクトの明示的解放(重要だが、これだけでは不十分であることを理解しておくこと)
Set doc = Nothing
' 5. CPU負荷軽減のための僅かな待機
DoEvents
Next i
' 全処理終了後、AutoCADを閉じる(Controllerがtaskkillするが、自ら閉じるのが紳士的)
ThisDrawing.Close False
End Sub
Private Sub PerformTransformation(ByRef doc As AcadDocument)
' ここに具体的な図面操作ロジックを記述
' 例: 全レイヤーの画層名変更、ブロック属性の更新など
Dim ent As AcadEntity
For Each ent In doc.ModelSpace
' 処理...
Next ent
End Sub
Private Function LoadFileList(path As String) As Variant
Dim fso As Object, ts As Object
Dim arr() As String, line As String, count As Long
Set fso = CreateObject("Scripting.FileSystemObject")
Set ts = fso.OpenTextFile(path, 1)
count = 0
Do Until ts.AtEndOfStream
ReDim Preserve arr(count)
arr(count) = ts.ReadLine
count = count + 1
Loop
ts.Close
LoadFileList = arr
End Function
Private Sub WriteLog(path As String, msg As String)
Dim fso As Object, ts As Object
Set fso = CreateObject("Scripting.FileSystemObject")
Set ts = fso.OpenTextFile(path, 8, True) ' 8 = ForAppending
ts.WriteLine Now & " : " & msg
ts.Close
End Sub
4. アーキテクトの視点:運用における極意
このアーキテクチャを実戦投入する際、以下の3点に留意せよ。
① Checkpoint(再開地点)の実装
大規模なバッチ処理は、夜間に実行されることが多い。途中で停電やネットワーク遮断が発生した場合に、最初からやり直すのは時間の浪費だ。
`process_log.txt` をスキャンし、「最後に成功したインデックス」を Controller が把握するロジックを組み込め。これにより、失敗しても「中断した箇所から再開」する、真にプロフェッショナルなバッチ処理が完成する。
② Windows APIによる「ゾンビプロセス」の根絶
AutoCADは、稀にダイアログボックスを表示したまま、あるいは致命的なエラーで応答なし(Not Responding)の状態になる。
Controller側では、`taskkill /F` を使用して、確実にプロセスを物理的に抹殺せよ。これは「行儀が悪い」ことではなく、「決定論的な制御」を実現するために不可欠な手段である。
③ メモリ使用量の監視
`intBatchSize`(再起動間隔)は、環境によって最適値が異なる。
Windowsのパフォーマンスモニター(PerfMon)を用いて、`acad.exe` の `Private Bytes` がどのように推移するかを計測せよ。メモリが線形に増加し、一定の閾値(例えば物理メモリの50%)に達する前にプロセスをリフレッシュするように設定するのが、最も効率的な戦略となる。
結論
VBAは強力だが、それはあくまで「AutoCADという宇宙の中の、小さな一画」を操作する手段に過ぎない。宇宙そのものが膨張し、重力(メモリ)が歪むとき、エンジニアに求められるのは、その宇宙を外側からコントロールする「設計思想」である。
「プロセスを使い捨て、状態を外部に持たせる」
この原則を胸に刻めば、君の自動化システムは、いかなる膨大なデータ群に対しても、揺るぎない堅牢性を発揮するだろう。
