大量図面処理における「AcadApplication」再起動タイマーの金字塔:メモリリークによる強制終了を完璧に防ぐエンタープライズ設計
数千枚から数万枚規模のDWG/DXFファイルを自動処理するバッチプログラムにおいて、現場のエンジニアを最も苦しめる障害——それが「処理が数百枚を超えたあたりで突然発生するAutoCADのフリーズ、RPCサーバー不可避エラー、およびサイレント強制終了」です。
どれほどVBA上で `AcadDocument.Close` を呼び出し、`Set objDoc = Nothing` でオブジェクトを明示的に解放したとしても、AutoCAD(`acad.exe`)の内部メモリ領域は確実に増大し続けます。これはVBAのガベージコレクションの限界ではなく、AutoCADが内部で抱えるObjectARX、グラフィックスキャッシュ(GS)、サードパーティ製ARXモジュール、およびC++のネイティブヒープ領域における「解放漏れ」という構造的宿命に起因します。
本稿では、レガシーかつミッションクリティカルなAutoCADバッチシステムを完全に制御下に置くため、Windows APIを駆使した「AcadApplicationプロセスの動的監視および自己クリーンアップ・再起起動(Process Recycling)アーキテクチャ」を解説します。
—
1. なぜ `AcadDocument.Close` だけでは `acad.exe` のメモリ死を防げないのか?
技術の真髄に踏み込む前に、`acad.exe` の内部で何が起きているのか、その病理を解剖します。
Native Heapの断片化とObjectARXの限界
AutoCADは純粋なC++アプリケーションであり、COMインターフェース(VBAやVB.NETから操作するラッパー)の配下には広大なネイティブヒープ領域が存在します。
DWGファイルを開く際、AutoCADは以下の処理を行います。
1. DWGデータベースの展開(ヘッダー、ブロック定義、シンボルテーブルのロード)
2. グラフィックスシステム(GS)パイプラインの構築(VBO、ディスプレイリストの生成)
3. レガシーARX / LISP アプリケーションのイベントハンドラ登録
ドキュメントを閉じた際、COMレベルのドキュメントオブジェクトは破棄されますが、C++レベルのグラフィックスキャッシュや一部の不揮発性ヒープ、フォントマッピングテーブル、拡張データ(XData/XRecord)のパーサーキャッシュはプロセスが終了するまで解放されません。
結果として、図面を開閉するごとに数MB〜数十MBのワーキングセット(Working Set)およびプライベートバイト(Private Bytes)が不可逆的にリークします。プロセスサイズが2GB(32bit)またはOSの限界(64bitでの物理メモリ・仮想メモリの上限)に達した瞬間、COM通信は `RPC_E_SERVERFAULT (0x80010105)` や `RPC_S_SERVER_UNAVAILABLE (0x800706BA)` を返して沈黙します。
結論:プロセスの「寿命」をコードで制御するしかない
この現象に対する唯一無二の解は、「定量的(処理枚数)または動的(物理メモリ消費量)にAutoCADプロセスそのものを定期的に殺し、クリーンな状態で再起動する」設計を採用することです。
AutoCAD内部のVBAから自身の親プロセス(`acad.exe`)を強制終了して再起動することは不可能です(VBA実行環境自体が道連れになるため)。したがって、外部オーケストレーター(Excel VBA、または独立したVB.NET/VBScript環境)からAutoCADをCOM操作し、制御する設計が絶対的要件となります。
—
2. アーキテクチャ設計:Win32 APIを用いた動的プロセスリサイクリング
本システムでは、以下の二重の制御ループを実装します。
1. 静的カウンタ制御: $N$ 枚(例: 50枚)処理ごとに `acad.exe` を正常終了・再起動する。
2. 動的メモリ閾値制御: Win32 API(`GetProcessMemoryInfo`)を用いて `acad.exe` のプライベートバイト(Private Bytes)をリアルタイム監視し、設定値(例: 1.5GB)を超えた瞬間に安全にプロセスをクリーンアップする。
ゾンビプロセスの確実な屠殺(Safe Kill Engine)
`AcadApplication.Quit` を呼んでも、ダイアログの応答待ちやCOMフリーズによりプロセスが残存(ゾンビ化)することがあります。そのため、`Quit` 発行後一定時間経過してもプロセスが生存している場合、Win32 APIの `TerminateProcess` を用いて物理的にプロセスの息の根を止める「防護策」を組み込みます。
—
3. 実装:完全版バッチコントローラー(Excel VBA / Out-of-Process COM)
以下のコードをExcelの標準モジュールに配置し、実行してください。参照設定なしで動作する「レイトブインディング(Late Binding)」および参照設定ありの「アーリーバインディング」の双方に耐えうる堅牢な記述を施しています。
Option Explicit
‘ ==============================================================================
‘ Win32 API 宣言 (64bit / 32bit 双方対応)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function OpenProcess Lib “kernel32” ( _
ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As LongPtr
Private Declare PtrSafe Function CloseHandle Lib “kernel32” ( _
ByVal hObject As LongPtr) As Long
Private Declare PtrSafe Function K32GetProcessMemoryInfo Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByRef pCounters As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
Private Declare PtrSafe Function GetWindowThreadProcessId Lib “user32” ( _
ByVal hWnd As LongPtr, _
ByRef lpdwProcessId As Long) As Long
Private Declare PtrSafe Function TerminateProcess Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal uExitCode As Long) As Long
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Function OpenProcess Lib “kernel32” ( _
ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Private Declare Function CloseHandle Lib “kernel32” ( _
ByVal hObject As Long) As Long
Private Declare Function K32GetProcessMemoryInfo Lib “kernel32” ( _
ByVal hProcess As Long, _
ByRef pCounters As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
Private Declare Function GetWindowThreadProcessId Lib “user32” ( _
ByVal hWnd As Long, _
ByRef lpdwProcessId As Long) As Long
Private Declare Function TerminateProcess Lib “kernel32” ( _
ByVal hProcess As Long, _
ByVal uExitCode As Long) As Long
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
‘ 権限フラグ
Private Const PROCESS_QUERY_INFORMATION As Long = &H400
Private Const PROCESS_VM_READ As Long = &H10
Private Const PROCESS_TERMINATE As Long = &H1
‘ メモリ情報構造体
Private Type PROCESS_MEMORY_COUNTERS
cb As Long
PageFaultCount As Long
PeakWorkingSetSize As LongPtr
WorkingSetSize As LongPtr
QuotaPeakPagedPoolUsage As LongPtr
QuotaPagedPoolUsage As LongPtr
QuotaPeakNonPagedPoolUsage As LongPtr
QuotaNonPagedPoolUsage As LongPtr
PagefileUsage As LongPtr
PeakPagefileUsage As LongPtr
End Type
‘ ==============================================================================
‘ 制御パラメーター定数
‘ ==============================================================================
Private Const MAX_FILES_PER_INSTANCE As Long = 50 ‘ 50枚ごとに無条件再起動
Private Const MEMORY_THRESHOLD_MB As Double = 1500# ‘ 1.5GB超過で緊急再起動
Private Const ACAD_PROGID As String = “AutoCAD.Application”
‘ ==============================================================================
‘ メイン処理オーケストレーター
‘ ==============================================================================
Public Sub Robust_AutoCAD_Mass_Processing()
Dim dwgFiles As Variant
Dim i As Long
Dim processCount As Long
Dim acadApp As Object
Dim acadPid As Long
Dim currentMemoryMB As Double
Dim dwgPath As String
‘ 1. 処理対象ファイルのリストアップ (ダミーパスリスト。実際はフォルダ検索等で取得)
dwgFiles = Array( _
“C:\Drawings\SiteA_001.dwg”, _
“C:\Drawings\SiteA_002.dwg”, _
“C:\Drawings\SiteA_003.dwg” _
/ 数千枚のパスがここに入る / _
)
On Error GoTo FatalErrorHandler
‘ 初期インスタンスの立ち上げ
Set acadApp = LaunchCleanAutoCAD(acadPid)
processCount = 0
For i = LBound(dwgFiles) To UBound(dwgFiles)
dwgPath = dwgFiles(i)
‘ — A. 再起動ロジックの判定 —
currentMemoryMB = GetProcessMemoryUsageMB(acadPid)
If processCount >= MAX_FILES_PER_INSTANCE Or currentMemoryMB > MEMORY_THRESHOLD_MB Then
Debug.Print “[SYSTEM] リフレッシュ条件合致: 処理数=” & processCount & “, メモリ=” & Format(currentMemoryMB, “0.0”) & “MB”
‘ プロセスの完全クリーンアップと再起動
TerminateAutoCADCompletely acadApp, acadPid
Set acadApp = LaunchCleanAutoCAD(acadPid)
processCount = 0
End If
‘ — B. 単一ドキュメントの安全処理 —
Debug.Print “[” & (i + 1) & “/” & (UBound(dwgFiles) + 1) & “] 処理中: ” & dwgPath
ProcessSingleDocument acadApp, dwgPath
processCount = processCount + 1
DoEvents
Next i
‘ 処理完了後の後片付け
TerminateAutoCADCompletely acadApp, acadPid
MsgBox “すべての図面処理が正常に完了しました。”, vbInformation, “処理完了”
Exit Sub
FatalErrorHandler:
MsgBox “致命的エラーが発生しました: ” & Err.Description, vbCritical, “エラー”
If Not acadApp Is Nothing Then
TerminateAutoCADCompletely acadApp, acadPid
End If
End Sub
‘ ==============================================================================
‘ AutoCAD起動&初期化(サイレントモード強制)
‘ ==============================================================================
Private Function LaunchCleanAutoCAD(ByRef outPid As Long) As Object
Dim app As Object
Dim hWnd As LongPtr
Debug.Print “[SYSTEM] AutoCADインスタンスを起動中…”
Set app = CreateObject(ACAD_PROGID)
app.Visible = False ‘ バックグラウンド実行
‘ システム変数の安全化 (UIダイアログの徹底抑止)
app.Preferences.System.EnableStartupDialog = False
‘ PIDの取得
hWnd = app.HWND
GetWindowThreadProcessId hWnd, outPid
Debug.Print “[SYSTEM] AutoCAD起動完了 PID: ” & outPid
Set LaunchCleanAutoCAD = app
End Function
‘ ==============================================================================
‘ DWG個別処理(例外捕捉とオブジェクト解放の徹底)
‘ ==============================================================================
Private Sub ProcessSingleDocument(ByRef app As Object, ByVal filePath As String)
Dim doc As Object
On Error GoTo DocErrorHandler
‘ ReadOnly:=False で開く
Set doc = app.Documents.Open(filePath, ReadOnly:=False)
‘ —– ここに本来の編集・解析ロジックを記述 —–
‘ 例: ダイアログ抑制用のシステム変数変更
doc.SetVariable “CMDECHO”, 0
doc.SetVariable “FILEDIA”, 0
‘ 何らかの図形操作等…
doc.Save
‘ ———————————————–
DocCleanup:
On Error Resume Next
If Not doc Is Nothing Then
doc.Close False ‘ 既に保存済みのためSaveChanges:=False
Set doc = Nothing
End If
‘ ガベージコレクションを促すための微小ウェイト
Sleep 100
Exit Sub
DocErrorHandler:
Debug.Print “[ERROR] ファイル処理失敗: ” & filePath & ” – ” & Err.Description
Resume DocCleanup
End Sub
‘ ==============================================================================
‘ プロセス強制終了を含む完全シャットダウン
‘ ==============================================================================
Private Sub TerminateAutoCADCompletely(ByRef app As Object, ByVal pid As Long)
#If VBA7 Then
Dim hProcess As LongPtr
#Else
Dim hProcess As Long
#End If
Dim timeoutCount As Long
Debug.Print “[SYSTEM] AutoCAD (PID: ” & pid & “) を破棄します…”
On Error Resume Next
If Not app Is Nothing Then
app.Quit
Set app = Nothing
End If
On Error GoTo 0
‘ プロセスが実際に終了するのを最大5秒待機
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_TERMINATE, 0, pid)
If hProcess <> 0 Then
timeoutCount = 0
Do While timeoutCount < 50
If GetProcessMemoryUsageMB(pid) = 0 Then Exit Do ' プロセス消滅
Sleep 100
timeoutCount = timeoutCount + 1
DoEvents
Loop
' まだ生きている場合は Win32 API で物理殺害 (Zombie Kill)
If GetProcessMemoryUsageMB(pid) > 0 Then
Debug.Print “[WARNING] Quit応答なし。PID: ” & pid & ” を強制終了(TerminateProcess)します。”
TerminateProcess hProcess, 0
End If
CloseHandle hProcess
End If
‘ OS側のメモリクリーンアップ待ち
Sleep 1000
End Sub
‘ ==============================================================================
‘ Win32 API によるメモリ消費量(Private Bytes相当)の取得 [MB単位]
‘ ==============================================================================
Private Function GetProcessMemoryUsageMB(ByVal pid As Long) As Double
#If VBA7 Then
Dim hProcess As LongPtr
#Else
Dim hProcess As Long
#End If
Dim pmc As PROCESS_MEMORY_COUNTERS
Dim memMB As Double
memMB = 0#
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, 0, pid)
If hProcess <> 0 Then
pmc.cb = LenB(pmc)
If K32GetProcessMemoryInfo(hProcess, pmc, pmc.cb) <> 0 Then
‘ PagefileUsage は Private Bytes (コミットされたメモリ量) を表す
memMB = CDbl(pmc.PagefileUsage) / (1024# 1024#)
End If
CloseHandle hProcess
End If
GetProcessMemoryUsageMB = memMB
End Function
—
4. プロフェッショナルが抑えるべき3つの極限最適化
上記プログラムを運用するにあたり、現場での失敗を防ぐための重要アーキテクチャノウハウを追加で提示します。
① システム変数の「ダイアログ爆弾」を無力化する
バッチ処理中に最も恐ろしいのは、フォント代替ダイアログやDWG参照の欠損に伴う「モーダルダイアログの出現」による処理停止です。
AutoCADインスタンスを生成した直後、またはドキュメントを開いた直後に以下のシステム変数をプログラムから確実に注入してください。
- `FILEDIA = 0`: ファイル選択ダイアログを禁止(コマンドライン応答化)
- `CMDECHO = 0`: コマンドプロンプトのエコーバックを無効化
- `EXPERT = 5`: 確認プロンプト(「本当に削除しますか?」等)を自動抑止
- `NOMUTT = 1`: メッセージ表示の完全ミュート(AutoCAD 2015以降)
② オブジェクト破棄の順序(LIFO: Last-In, First-Out)
VBAの参照カウントを正常に機能させるため、オブジェクトの解放は生成と逆の順序で行う必要があります。
‘ 正しい解放手順の例
Set objEntity = Nothing
Set objModelSpace = Nothing
objDocument.Close False
Set objDocument = Nothing
上位階層である `AcadDocument` や `AcadApplication` を先に `Nothing` にすると、下位の `AcadEntity` のCOMラッパーがメモリ内に孤立(Orphaned Object)し、プロセスを終了するまで二度と開放されないリーク領域となります。
③ ObjectDBX(`AxDbDocument`)の限定的採用
もしバッチ処理の内容が「図面の表示・描画更新を伴わない、属性文字の書き換えやレイアウト情報の読み取りのみ」である場合、`AcadDocument` ではなく ObjectDBX(`AxDbDocument`) を使用することを強く推奨します。
ObjectDBXはAutoCADのGUI(グラフィックスエンジン)をバイパスしてDWGデータベースに直接アクセスするため、処理速度は10倍以上向上し、メモリ消費量も大幅に抑えられます。ただし、ObjectDBXでもリークはゼロにはならないため、本稿で紹介した「定期的なインスタンスリセット」の枠組み自体は必須です。
—
結論
レガシーなAutoCAD VBA環境において、「メモリリークを起こさない完全無欠なコード」を書くことは不可能です。AutoCADの構造上、ネイティブ層でのリークをVBA側から制御することはできません。
真のシステムアーキテクトが取るべきアプローチは、「リークは不可避である」という現実を受け入れ、それが障害に発展する前にシステム自体の「代謝(プロセスの破棄と再生成)」を自動化することです。
本稿で示したWin32 APIを融合させた再起動エンジンを導入することで、あなたのバッチシステムは数万枚の図面であっても、途中で止まることなく全自動で完走する圧倒的な堅牢性を手に入れることになります。
