限界を超えろ:大量図面処理におけるGDIリソース枯渇への対抗策と、自己修復型オートメーション設計
数百、時には数千のDWGファイルを一括処理するツールを開発する際、多くのエンジニアが陥る「見えない壁」がある。
「最初は完璧に動いていた。しかし、200枚を超えたあたりから動作が重くなり、最終的にはAutoCADが『メモリ不足』あるいは『予期せぬエラー』でクラッシュする。」
もし君がこのような現象に直面しているなら、君はまだ「コードを書くこと」に終始しており、「システムのライフサイクルを管理すること」ができていない。原因は単純なメモリ(RAM)不足ではない。GDI(Graphics Device Interface)リソースの枯渇だ。
本稿では、プロフェッショナルとして、単なる「図面処理スクリプト」を「堅牢な業務基盤」へと昇華させるための、極限の設計思想と実装手法を伝授する。
1. なぜ「メモリ」ではなく「GDI」が死を招くのか
多くの開発者は、メモリ使用量(Working Set)だけを監視すれば良いと考えている。しかし、AutoCADのような高度なグラフィックエンジンを搭載したアプリケーションにおいて、真のボトルネックとなるのはGDIハンドルである。
GDIリソースとは
Windowsが描画(フォント、ペン、ブラシ、ビットマップなど)を行うために使用するシステムリソースだ。AutoCADが図面を開き、オブジェクトを描画し、レイヤーやスタイルを操作するたびに、これらGDIハンドルは消費される。
致命的な誤解:COMオブジェクトの解放
VBAで `Set obj = Nothing` と書けば、オブジェクトは解放される。しかし、それはあくまでCOMの参照カウントを減らすだけであり、Windows OSレベルで確保されたGDIハンドルが即座に、かつ確実に返却される保証はない。
大量の図面を連続で処理すると、メモリは空いているように見えても、OSのGDIハンドル上限(プロセスあたり通常10,000個程度)に達し、AutoCADは描画不能に陥り、沈黙する。これが「リソースリークによるクラッシュ」の正体だ。
2. 堅牢な設計思想:コントローラー・ワーカー・パターン
単一のVBAマクロ内で全てのループを完結させる設計は、プロフェッショナルとしては「脆弱」と断じる。なぜなら、一度クラッシュすれば、どこまで処理が進んだかのコンテキストを失うからだ。
我々が採用すべきは、「コントローラー(管理責任者)」と「ワーカー(実行部隊)」を分離したアーキテクチャである。
1. Controller (Excel VBA or VBScript):
- 処理すべき図面リストの管理(DBやCSVから取得)。
- GDIリソースの監視。
- AutoCADプロセスの起動・終了の制御。
- 進捗の永続化(どこまで終わったかをログに記録)。
2. Worker (AutoCAD VBA):
- 単一の図面を開き、処理を行い、保存して閉じる。
- 「一回限りのミッション」に特化する。
この分離により、リソースが限界に達した際、ControllerはWorker(AutoCAD)を安全に終了させ、新しいプロセスを立ち上げて、中断した箇所から再開することができる。
3. 実装:GDI監視と自己修復ロジック
以下に、Windows APIを用いたGDIリソースの監視と、それに基づいた制御ロジックのプロトタイプを示す。ここでは、Controller側(Excel等)で動作することを想定した設計とする。
Windows APIによるリソース監視モジュール
' --- Module: Win32API_Monitor ---
Option Explicit
If VBA7 Then
' 64bit版AutoCAD/Office対応
Declare PtrSafe Function GetGuiResources Lib "user32" (ByVal hdc As LongPtr) As Long
Else
' 32bit版対応
Declare Function GetGuiResources Lib "user32" (ByVal hdc As Long) As Long
End If
' GDIリソースの現在の使用量を取得する
' hdc には 0 (GetDC(0)) を渡すことでシステム全体のコンテキストを取得可能だが、
' プロセス単位の監視が基本となる。
Public Function GetCurrentGDICount() As Long
' 0を指定することで、呼び出しプロセス(この場合はExcel/Controller)のGDIハンドル数を取得
GetCurrentGDICount = GetGuiResources(0)
End Function
プロフェッショナル・メインループ(Controller側)
このコードは、単なるループではない。「限界を察知し、次世代へバトンを渡す」ためのロジックだ。
' --- Module: MainController ---
Option Explicit
Private Const GDI_THRESHOLD As Long = 8000 ' 警告レベル(OS上限の手前で設定)
Private Const LOG_FILE As String = "C:\Temp\ProcessingLog.txt"
Sub ExecuteMassProcessing()
Dim drawingList As Collection
Set drawingList = LoadDrawingList() ' 未処理の図面リストを取得
Dim acadApp As Object
Dim currentDWG As Variant
Do While drawingList.Count > 0
' 1. AutoCADの起動・接続
Set acadApp = GetOrStartAutoCAD()
If acadApp Is Nothing Then
MsgBox "AutoCADの起動に失敗しました。", vbCritical
Exit Sub
End If
' 2. 図面処理ループ
On Error Resume Next
Do While drawingList.Count > 0
' --- 【極めて重要】リソース監視 ---
If GetCurrentGDICount() > GDI_THRESHOLD Then
Debug.Print "GDIリソース限界を検知。セッションを再起動します..."
Exit Do ' 内側のループを抜け、AutoCADを再起動させる
End If
currentDWG = drawingList(1)
' Worker(AutoCAD内マクロ)を呼び出し
' 引数として図面パスを渡す
Call RunWorkerMacro(acadApp, CStr(currentDWG))
' 処理完了後の後処理
If Err.Number = 0 Then
' 成功したらリストから削除し、ログに記録
UpdateProgressLog currentDWG, "SUCCESS"
drawingList.Remove 1
Else
' 失敗した場合はログにエラーを記録して次へ(または中断)
UpdateProgressLog currentDWG, "ERROR: " & Err.Description
drawingList.Remove 1
Err.Clear
End If
Loop
On Error GoTo 0
' 3. リソース枯渇によるループ脱出時、AutoCADを安全に終了させる
Debug.Print "AutoCADセッションをクリーンアップします。"
Call SafeTerminateAutoCAD(acadApp)
Set acadApp = Nothing
' 次のセッションへ(drawingListが空なら終了)
Loop
MsgBox "全工程が完了しました。", vbInformation
End Sub
' AutoCADインスタンスの取得または生成
Private Function GetOrStartAutoCAD() As Object
On Error Resume Next
' 既存のインスタンスを探す
Set GetOrStartAutoCAD = GetObject(, "AutoCAD.Application")
' なければ新規作成
If Err.Number <> 0 Then
Err.Clear
Set GetOrStartAutoCAD = CreateObject("AutoCAD.Application")
GetOrStartAutoCAD.Visible = True
End If
On Error GoTo 0
End Function
' AutoCADを強制終了させず、安全に閉じる
Private Sub SafeTerminateAutoCAD(ByRef app As Object)
On Error Resume Next
' 図面が開いていればすべて閉じる
Dim doc As Object
For Each doc In app.Documents
doc.Close False ' 保存せずに閉じる(進捗はログで管理しているため)
Next doc
app.Quit
Set app = Nothing
End Sub
4. 実務における「落とし穴」と回避策
データベース・ログ連携の重要性
上記のコードでは `UpdateProgressLog` という関数を仮定している。これは、単なるテキストファイルではなく、「処理済みフラグ」を管理するデータベース(SQLiteやAccess、あるいは単純なCSV)であるべきだ。
なぜなら、ハードウェアの不具合や停電でマシンが落ちた際、Controller自体が死ぬ可能性があるからだ。再起動した際、そのログを見て「どこから再開すべきか」を判断できなければ、二重処理(図面への書き込み重複)を招く。
バージョン互換性の制御
大量処理において、古いバージョンのDWGを扱う場合は、Worker側で `SaveAs` を使用して、明示的に新しいバージョンへ変換するロジックを組み込むこと。
' Worker側での例
doc.SaveAs "C:\Path\To\NewFile.dwg", acNative // 常に最新形式で保存
タイムアウト設計
GDIリソースが枯渇し始めると、AutoCADの応答が極端に遅くなる。`GetObject` や `CreateObject` がハングアップすることを見越し、Controller側には必ず「一定時間応答がない場合はプロセスを強制終了(`taskkill`)する」といった、より強硬なタイムアウト処理を検討すべきだ。
結論
プロフェッショナルな自動化エンジニアとは、コードが「正常に動く時」のことだけを考えている者ではない。「システムが限界に達した時、いかにエレガントに、かつ確実に業務を継続させるか」を設計できる者のことだ。
GDIリソースの監視、Controller-Worker分離、そして永続的な進捗管理。これらを実装に組み込むことで、君のツールは単なるスクリプトから、プロフェッショナルの現場に耐えうる「信頼のインフラ」へと進化する。
「止まらないシステム」を作れ。それが、我々エンジニアの責務である。
