MS Project VBAを極限まで安定化させる:リソースオブジェクトのメモリリーク根絶とライフサイクル管理の深淵
レガシーな基幹システムや、何千ものタスクと複雑なリソースプールを持つ巨大な工程表群。それらをVBAで自動制御する際、シニアエンジニアの前に必ず立ちふさがる壁がある。それが「メモリリークによる突然の強制終了(プロセス消滅)」だ。
特に、数時間から数日にわたるバッチ処理、あるいは外部システムとの連携で何百回もMS Projectのインスタンスやリソースを操作する自動化ツールにおいて、適切なメモリ解放が行われていないコードは、時間の問題でクラッシュする。
本稿では、Project VBAにおけるリソースオブジェクト(`Resource`、`Assignment`)のライフサイクルを完全に掌握し、COMコンポーネントの背後にあるC++のメモリ構造まで意識した「真に安定稼働する自動化アーキテクチャ」を提示する。
—
1. MS Project VBAにおけるメモリリークの根本原因
VBAはガベージコレクション(GC)言語ではない。しかし、COM(Component Object Model)オブジェクトに関しては、参照カウント方式によるライフサイクル管理が行われている。
ここで多くの開発者が陥る致命的な罠がある。それは、「VBAの変数スコープを抜ければ勝手に解放される」という幻想だ。
‘ 【アンチパターン】一見問題なさそうに見えて、メモリリークを引き起こすコード
Sub BadResourceAllocation()
Dim prjProj As Project
Set prjProj = ActiveProject
Dim res As Resource
For Each res In prjProj.Resources
If Not res Is Nothing Then
‘ リソースのコストや稼働率を操作
res.StandardRate = “5000/h”
End If
Next res
‘ ループを抜けても、裏でCOMの参照が残留するケースがある
End Sub
上記のコードを何千回ものループや、イベントドリブンな常駐型マクロの中で実行すると、VBAランタイムとMS Project(`winproj.exe`)のプロセス間で参照カウンタの不整合が生じ、メモリ使用量が右肩上がりに増加していく。
プロフェッショナルが守るべき鉄則
1. オブジェクト変数は明示的に `Nothing` を代入して解放する
2. コレクションのイテレーション(`For Each`)内暗黙的参照を断ち切る
3. 長寿命の処理では、適切なタイミングで `GarbageCollection`(VBA自体のメモリ解放サイクル)を意識させる
—
2. リソースオブジェクトの安全な解放と参照切断の実装パターン
リソースの登録、割り当て(Assignment)、コスト管理を安全に行うためには、オブジェクト変数のスコープを極限まで小さくし、使い捨てた端から明示的に破棄する構造が求められる。
以下のコードは、数千件のリソースプールに対して安全にコスト調整とアサインメントの最適化を行うプロフェッショナル向けの実装例である。
Option Explicit
‘ Windows APIを用いたメモリ最適化(必要に応じプロセスのワーキングセットを縮小)
If VBA7 Then
Declare PtrSafe Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal dwMinimumWorkingSetSize As LongPtr, _
ByVal dwMaximumWorkingSetSize As LongPtr) As Long
Declare PtrSafe Function GetCurrentProcess Lib “kernel32” () As LongPtr
Else
Declare Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As Long, _
ByVal dwMinimumWorkingSetSize As Long, _
ByVal dwMaximumWorkingSetSize As Long) As Long
Declare Function GetCurrentProcess Lib “kernel32” () As Long
End If
Sub ProfessionalResourceManagement()
Dim prjProj As Project
Set prjProj = ActiveProject
Dim resColl As Resources
Set resColl = prjProj.Resources
Dim lngCount As Long
lngCount = resColl.Count
Dim i As Long
Dim targetRes As Resource
Dim targetAsg As Assignment
On Error GoTo ErrorHandler
For i = 1 To lngCount
‘ インデックスアクセスにより、For Eachによる暗黙的なCOM参照のリークを防ぐ
Set targetRes = resColl(i)
If Not targetRes Is Nothing Then
If targetRes.Type = pjResourceTypeWork Then
‘ 稼働率やコストの調整ロジック
targetRes.StandardRate = “4000/h”
‘ リソースに紐付くアサインメント(Assignment)の処理
For Each targetAsg In targetRes.Assignments
‘ アサインメント固有のコスト上書きや調整
targetAsg.Cost = targetAsg.Work 4000
‘ 個別オブジェクトの即時解放
Set targetAsg = Nothing
Next targetAsg
End If
End If
‘ ループの都度、確実にリソース変数を解放
Set targetRes = Nothing
‘ 100件ごとにVBAのメモリ領域を整理(揺らぎを防ぐ)
If i Mod 100 = 0 Then
DoEvents
End If
Next i
CleanUp:
‘ コレクションおよびプロジェクト参照の解放
Set resColl = Nothing
Set prjProj = Nothing
‘ Windows APIでプロセスのメモリフットプリントを最適化
Call FlushMemory
Exit Sub
ErrorHandler:
MsgBox “エラー発生: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
Private Sub FlushMemory()
‘ OSに対してメモリの解放を要求(スワップアウト促進)
Dim lngResult As Long
lngResult = SetProcessWorkingSetSize(GetCurrentProcess(), -1&, -1&)
End Sub
—
3. レガシー環境・システム間連携における極限の安定化
外部のERPやExcel、SQL Server等からCSVやJSON経由で大量のリソースデータを受け取り、MS Projectへ流し込むシステム間連携基盤では、さらに高度なリソース管理が要求される。
COM例外と「ゾンビプロセス」の回避
自動化スクリプトが途中でエラー落ちした際、背後で `winproj.exe` がタスクマネージャーに残存し続ける(ゾンビプロセス化)現象に悩まされたアーキテクトは多いはずだ。これは、親アプリケーション(ExcelやVBAランタイム)から掴んだCOMオブジェクトのハンドルが完全に解放されていないことが原因である。
これを防ぐためには、エラーハンドリングの網羅性と確実なオブジェクト破棄の保証(Finally句に相当するクリーンアップブロック)が絶対条件となる。
Sub EnterpriseResourceSyncEngine()
Dim appProj As MSProject.Application
Dim prj As Project
Dim isAppCreated As Boolean
On Error GoTo SafeExit
‘ MS Projectのインスタンス取得(起動していなければ新規作成)
On Error Resume Next
Set appProj = GetObject(, “MSProject.Application”)
If appProj Is Nothing Then
Set appProj = New MSProject.Application
isAppCreated = True
End If
On Error GoTo SafeExit
appProj.Visible = False ‘ バックグラウンド実行でパフォーマンス向上
Set prj = appProj.ActiveProject
If prj Is Nothing Then
Set prj = appProj.Projects.Add
End If
‘ — ここに外部DB/CSVからのリソース登録・コスト調整ロジックを記述 —
‘ (前述のインデックスアクセスと `Nothing` 代入を徹底すること)
If isAppCreated Then
appProj.Quit pjDoNotSave
End If
SafeExit:
‘ 異常終了時も含めて必ず実行されるクリーンアップ
If Err.Number <> 0 Then
MsgBox “致命的なエラー: ” & Err.Description, vbCritical
End If
‘ 逆順かつ確実に解放
Set prj = Nothing
If Not appProj Is Nothing Then
If isAppCreated Then
On Error Resume Next
appProj.Quit pjDoNotSave
On Error GoTo 0
End If
Set appProj = Nothing
End If
Call FlushMemory
End Sub
—
4. チーフアーキテクトからの提言
Project VBAにおけるリソース管理の本質は、「VBAの簡易的な構文に騙されず、背後でうごめくCOMのライフサイクルを脳内で完全にシミュレーションすること」にある。
- `For Each` は便利だが、巨大なリソースプールやアサインメントの走査においては、メモリリークの温床になり得るため、極力インデックスアクセス(`Collection(i)`)へ置き換える。
- ループ内でのオブジェクト生成・破棄のサイクルを最小化し、不要になった瞬間に `Set xxx = Nothing` を叩く。
- 定期的な `DoEvents` や Windows API (`SetProcessWorkingSetSize`) の活用により、OS側のメモリプレッシャーをコントロールする。
これらの泥臭くも妥協のないエンジニアリングの積み重ねこそが、数千時間の稼働に耐えうる、真に堅牢なEnterprise VBAシステムの基盤となる。退屈なコードに別れを告げ、メモリを完全掌握せよ。
