レガシーの呪縛を解放する「DynamicWrapperX」という劇薬
Windows Serverの運用現場や長年保守され続けているエンタープライズシステムにおいて、VBScript(Windows Script Host: WSH)はいまだに強力なフットプリントを維持している。しかし、ネイティブなVBScriptには決定的な弱点が存在する。すべての変数が `Variant` 型に強制され、メモリへの直接アクセスや Win32 API の直接呼び出しが言語仕様として封印されている点だ。
通常、Win32 APIを利用するにはC/C++でのDLL開発や、VBA(Visual Basic for Applications)の `Declare` 構文に頼る必要がある。だが、WSH実行環境において追加のコンパイル作業や巨大なランタイムを導入することなく、OSのカーネルレベル機能へアクセスする最適解が存在する。それが DynamicWrapperX (`dynwrapx.dll`) だ。
DynamicWrapperXは、VBScriptの内部オブジェクト(COM)とWin32 DLLのエントリポイントとの間に立ち、動的にスタックフレームを構築・アライメントして関数を呼び出す軽量なブリッジコンポーネントである。
本稿では、単なるAPI呼出のサンプル提示にとどまらず、DynamicWrapperXの内部機構、呼び出し規約(Calling Convention)、メモリバッファの直接操作、そしてリソースリークを極限まで防ぐアーキテクチャ設計について深く解説する。
—
環境構築と内部アーキテクチャの解剖
1. 32bit / 64bit 実行環境の分離と登録
DynamicWrapperX を使用する上で、最もエンジニアを悩ませるのが Windows OS の WOW64(Windows on Windows 64-bit)サブシステムと DLL のビット数一致である。
- 32bit版 DLL (`dynwrapx.dll`) $\rightarrow$ `C:\Windows\SysWOW64\wscript.exe` (または `cscript.exe`) で実行
- 64bit版 DLL (`dynwrapx64.dll`) $\rightarrow$ `C:\Windows\System32\wscript.exe` (または `cscript.exe`) で実行
コンポーネントの登録コマンド(管理者権限が必要)
:: 32bit環境向け登録
%SystemRoot%\SysWOW64\regsvr32.exe dynwrapx.dll
:: 64bit環境向け登録
%SystemRoot%\System32\regsvr32.exe dynwrapx64.dll
2. データ型マッピングと呼び出し規約(Calling Convention)
DynamicWrapperX の `Register` メソッドは、DLL名、関数名、引数の型(`i`)、戻り値の型(`r`)、呼び出し規約(`f`)を指定してAPIを登録する。
DynamicWrapperX 主要型変換コード一覧
| 型指定 | C/C++ ネイティブ型 | VBScript 側の対応表現 | 説明 |
| :— | :— | :— | :— |
| `l` | `LONG` / `int` (32bit) | Long (Integer) | 32ビット符号付き整数 |
| `u` | `UINT` / `DWORD` (32bit) | Long | 32ビット符号なし整数 |
| `p` | `LPVOID` / ポインタ | Long / LongLong | メモリアドレス(32bit/64bit注意) |
| `s` | `LPCSTR` (ANSI String) | String | ANSI文字列(自動変換) |
| `w` | `LPCWSTR` (Unicode String) | String | UTF-16 Unicode文字列 |
| `h` | `HANDLE` / `HWND` | Long / LongPtr | カーネルハンドル / ウィンドウハンドル |
| `q` | `LARGE_INTEGER` (64bit) | Currency / Double | 64ビット整数 |
呼び出し規約はデフォルトで `__stdcall` (`f=r`) となるが、Cランタイム(msvcrt.dll等)の `__cdecl` 呼び出しを行う場合は `f=c` を明示しなければスタック崩壊(Stack Corruption)を引き起こす。
—
実戦編Ⅰ:高精度タイマーによるマイクロ秒単位のパフォーマンス計測
標準の `Timer` 関数はミリ秒単位の精度しか持たず、OSのスケジューリング粒度に依存する。システムのボトルネックを極限まで割り出すため、`Kernel32.dll` の `QueryPerformanceCounter` および `QueryPerformanceFrequency` を直接呼び出す高精度タイマーを実装する。
‘==============================================================================
‘ HighResolutionTimer.vbs
‘ 概要: DynamicWrapperX を用いた Win32 QueryPerformanceCounter の直接実行
‘ 実行環境: cscript.exe (SysWOW64 または System32)
‘==============================================================================
Option Explicit
Const DX_CLASS = “DynamicWrapperX”
Dim DX
Set DX = CreateObject(DX_CLASS)
‘ Win32 API の登録
‘ LONG_PTR (64bit整数ポインタ) を受け取るため ‘p’ を使用
DX.Register “kernel32.dll”, “QueryPerformanceFrequency”, “i=p”, “r=l”
DX.Register “kernel32.dll”, “QueryPerformanceCounter”, “i=p”, “r=l”
Dim freq, t1, t2
freq = GetSystemFrequency(DX)
WScript.Echo “[INFO] CPU Counter Frequency: ” & freq & ” Hz”
‘ 計測対象ブロック開始
t1 = GetCurrentCounter(DX)
‘ 重い処理のシミュレーション(例: 文字列連結の反復)
Dim i, dummy
For i = 1 To 100000
dummy = “String_” & i
Next
t2 = GetCurrentCounter(DX)
‘ 計測対象ブロック終了
‘ 経過時間の計算 (秒単位 -> マイクロ秒に変換)
Dim elapsedTime
elapsedTime = ((t2 – t1) / freq) 1000000
WScript.Echo “[RESULT] Execution Time: ” & FormatNumber(elapsedTime, 2) & ” microseconds (μs)”
‘ オブジェクトの完全破棄
Set DX = Nothing
‘——————————————————————————
‘ 内部関数: QueryPerformanceFrequency の呼び出し
‘——————————————————————————
Function GetSystemFrequency(byRef objDX)
Dim buf
buf = Space(8) ‘ 64bit integer (8 bytes) バッファの確保
Dim ret
ret = objDX.QueryPerformanceFrequency(buf)
If ret = 0 Then
Err.Raise vbObjectError + 512, “Win32API”, “QueryPerformanceFrequency Failed.”
End If
GetSystemFrequency = UnpackInt64(buf)
End Function
‘——————————————————————————
‘ 内部関数: QueryPerformanceCounter の呼び出し
‘——————————————————————————
Function GetCurrentCounter(byRef objDX)
Dim buf
buf = Space(8)
Dim ret
ret = objDX.QueryPerformanceCounter(buf)
If ret = 0 Then
Err.Raise vbObjectError + 513, “Win32API”, “QueryPerformanceCounter Failed.”
End If
GetCurrentCounter = UnpackInt64(buf)
End Function
‘——————————————————————————
‘ 内部関数: バイナリバッファ(8バイト)から 64bit 整数(Currency換算)へデコード
‘——————————————————————————
Function UnpackInt64(byRef strBuf)
Dim i, low, high, value
low = 0: high = 0
‘ 下位4バイトの復元
For i = 4 To 1 Step -1
low = low 256 + Asc(Mid(strBuf, i, 1))
Next
‘ 上位4バイトの復元
For i = 8 To 5 Step -1
high = high 256 + Asc(Mid(strBuf, i, 1))
Next
‘ VBScriptの制約上、近似値演算またはCurrency変換を実施
UnpackInt64 = (high (2^32)) + low
End Function
—
実戦編Ⅱ:構造体パッキングと `GlobalMemoryStatusEx` によるシステムメモリ監視
Win32 APIの真髄は、構造体(`STRUCT`)を介した大量のシステムパラメータの取得にある。VBScriptには構造体型が存在しないが、「メモリバッファ(Byte配列またはString)を自前で割り当て、オフセット計算によって値を読み出す」 パディングテクニックで攻略する。
ここでは `Kernel32.dll` の `GlobalMemoryStatusEx` を使用し、物理メモリの使用状況をダイレクトに取得する。
‘==============================================================================
‘ MemoryMonitor.vbs
‘ 概要: 構造体 MemoryStatusEx (64bytes) の直接パッキングと物理メモリ情報の取得
‘==============================================================================
Option Explicit
Dim DX
Set DX = CreateObject(“DynamicWrapperX”)
‘ GlobalMemoryStatusEx(LPMEMORYSTATUSEX lpBuffer)
DX.Register “kernel32.dll”, “GlobalMemoryStatusEx”, “i=p”, “r=l”
‘ 構造体 MEMORYSTATUSEX のサイズは 64 バイト (32bit/64bit共用)
‘ C++ Struct Alignment:
‘ DWORD dwLength (4)
‘ DWORD dwMemoryLoad (4)
‘ DWORDLONG ullTotalPhys (8)
‘ DWORDLONG ullAvailPhys (8)
‘ … 続く
Dim structSize
structSize = 64
‘ Null文字で埋められた64バイトのバッファメモリを構築
Dim buf
buf = String(structSize, Chr(0))
‘ 構造体の先頭4バイト(dwLength)に構造体自身のサイズ(64)をLittle-Endianで設定
buf = SetDWord(buf, 1, structSize)
‘ API実行
Dim res
res = DX.GlobalMemoryStatusEx(buf)
If res = 0 Then
WScript.Echo “[ERROR] GlobalMemoryStatusEx Call Failed.”
WScript.Quit 1
End If
‘ オフセットから各パラメータを抽出
Dim memoryLoad, totalPhys, availPhys
memoryLoad = GetDWord(buf, 5) ‘ Offset 4 (1-based index: 5)
totalPhys = GetQWord(buf, 9) ‘ Offset 8 (1-based index: 9)
availPhys = GetQWord(buf, 17) ‘ Offset 16 (1-based index: 17)
WScript.Echo “=== SYSTEM MEMORY STATUS ===”
WScript.Echo “Memory Load : ” & memoryLoad & ” %”
WScript.Echo “Total Physical : ” & FormatNumber(totalPhys / (1024 1024), 0) & ” MB”
WScript.Echo “Available Physical: ” & FormatNumber(availPhys / (1024 1024), 0) & ” MB”
Set DX = Nothing
‘——————————————————————————
‘ 内部関数: バッファの特定位置に DWORD (4バイト) を書き込む
‘——————————————————————————
Function SetDWord(byVal buffer, byVal offset, byVal val)
Dim b1, b2, b3, b4
b1 = Chr(val And &HFF)
b2 = Chr((val \ &H100) And &HFF)
b3 = Chr((val \ &H10000) And &HFF)
b4 = Chr((val \ &H1000000) And &HFF)
Mid(buffer, offset, 4) = b1 & b2 & b3 & b4
SetDWord = buffer
End Function
‘——————————————————————————
‘ 内部関数: バッファの特定位置から DWORD (4バイト) を読み出す
‘——————————————————————————
Function GetDWord(byRef buffer, byVal offset)
Dim i, val
val = 0
For i = offset + 3 To offset Step -1
val = val 256 + Asc(Mid(buffer, i, 1))
Next
GetDWord = val
End Function
‘——————————————————————————
‘ 内部関数: バッファの特定位置から DWORDLONG (8バイト) を読み出す
‘——————————————————————————
Function GetQWord(byRef buffer, byVal offset)
Dim low, high
low = GetDWord(buffer, offset)
high = GetDWord(buffer, offset + 4)
GetQWord = (high (2^32)) + low
End Function
—
実戦編Ⅲ:`User32.dll` を介した高度なウィンドウ探索と制御
標準の `WScript.Shell.AppActivate` や `SendKeys` は動作が非常に不安定であり、エンタープライズの自動化現場では採用できない。Win32 APIの `FindWindowExW` および `SendMessageW` を直接叩くことで、完全かつ確定的なウィンドウ制御を実現する。
対象ウィンドウのハンドルの特定と非表示テキストの全取得
‘==============================================================================
‘ WindowControl.vbs
‘ 概要: User32.dll によるターゲットウィンドウの列挙と制御
‘==============================================================================
Option Explicit
Dim DX
Set DX = CreateObject(“DynamicWrapperX”)
‘ Win32 API 登録 (Unicode版 ‘W’ を明示指定)
DX.Register “user32.dll”, “FindWindowExW”, “i=hhww”, “r=h”
DX.Register “user32.dll”, “SendMessageW”, “i=hhll”, “r=l”
DX.Register “user32.dll”, “GetWindowTextW”, “i=hpw”, “r=l”
DX.Register “user32.dll”, “IsWindowVisible”, “i=h”, “r=l”
Dim hwndTarget
‘ メモ帳(Notepad)のウィンドウハンドルを取得
‘ ClassName: “Notepad”, WindowName: Empty
hwndTarget = DX.FindWindowExW(0, 0, “Notepad”, vbNullString)
If hwndTarget = 0 Then
WScript.Echo “[WARN] Target window (Notepad) not found.”
WScript.Quit
End If
‘ ウィンドウが可視状態かチェック
If DX.IsWindowVisible(hwndTarget) <> 0 Then
WScript.Echo “[INFO] Found Visible Window Handle: 0x” & Hex(hwndTarget)
End If
‘ メモ帳内部のエディタコントロール (“Edit”) のハンドルを取得
Dim hwndEdit
hwndEdit = DX.FindWindowExW(hwndTarget, 0, “Edit”, vbNullString)
If hwndEdit <> 0 Then
WScript.Echo “[INFO] Found Edit Control Handle: 0x” & Hex(hwndEdit)
‘ WM_SETTEXT (0x000C) を送信してテキストを物理注入
Const WM_SETTEXT = &H000C
Dim textToInject, ptrText
textToInject = “Kernel-level injection via DynamicWrapperX.” & vbCrLf & “Architected by Lead Engineer.”
‘ DynamicWrapperX の Register 経由で文字列の直接渡しも可能だが、
‘ メモリポインタ経由で確実な制御を行う
DX.Register “user32.dll”, “SendMessageW”, “i=hhlw”, “r=l”
DX.SendMessageW hwndEdit, WM_SETTEXT, 0, textToInject
WScript.Echo “[SUCCESS] Text injected successfully.”
Else
‘ Windows 11 の Notepad など、Edit クラス構造が変わっている場合の対処
WScript.Echo “[WARN] Edit control handle not found. (RichEditD2DPT / New Notepad Structure?)”
End If
Set DX = Nothing
—
メモリ最適化・リソース解放・信頼性設計の鉄則
Win32 APIを直接操作するVBScriptは、もはや「安全なスクリプト言語」ではない。一歩間違えれば、メモリリーク(Orphaned Memory)、Access Violation(`0xC0000005`)、クラッシュを引き起こす。以下の極限の鉄則をコードへ組み込まなければならない。
1. COMオブジェクトおよびアンマネージドハンドルの完全な明示的解放
VBScriptのガベージコレクション(参照カウント方式)は、スクリプト終了時に解放処理を行うが、連続実行されるスクリプトやサービス環境のタスクにおいては、即座に解放しなければプロセス空間を圧迫する。
‘ API呼び出し用のラッパークラスパターン
Class Win32Engine
Private m_DX
Private Sub Class_Initialize()
Set m_DX = CreateObject(“DynamicWrapperX”)
‘ 必要なAPIをイニシャライズ時に一括登録
m_DX.Register “kernel32.dll”, “CloseHandle”, “i=h”, “r=l”
End Sub
Private Sub Class_Terminate()
‘ 明示的にオブジェクト参照を切り、COMコンポーネントの Release を強制
If IsObject(m_DX) Then
Set m_DX = Nothing
End If
End Sub
Public Function CloseKernelHandle(byVal hHandle)
If hHandle <> 0 Then
CloseKernelHandle = m_DX.CloseHandle(hHandle)
End If
End Function
End Class
2. ポインタの生存期間(Lifetime)と文字列バッファのアライメント
- BSTR と LPWSTR の混同を避ける: VBScriptの `String` は内部的に `BSTR` (長さ前置 Unicode 文字列) である。`LPCWSTR` を求める Win32 API にそのまま渡す場合、DynamicWrapperX の型指定 `w` を使用すれば自動ブリッジされるが、直接ポインタ(`p`)として渡す場合は `StrPtr` 相当の操作を意識し、呼び出し中の文字列バッファの再割り当て(変数の再代入)を絶対に行ってはならない。
- 構造体のアライメント問題: C/C++の `Pragma pack` やコンパイラ最適化によるパディング(4バイト境界/8バイト境界)を正確に計算してバッファ(`String` や `Byte Array`)のサイズを計算しなければ、API内部でスタックオーバーランが発生し、ホストプロセス(`cscript.exe`)が即座に落ちる。
3. エラーハンドリングの要塞化 (`SetLastError` と `GetLastError`)
Win32 APIが失敗した際、原因を追求するには `Kernel32.dll` の `GetLastError` を直ちに取得しなければならない。VBScriptの `Err.LastDllError` は `Declare` 構文経由でないと正しく更新されない場合があるため、`GetLastError` も DynamicWrapperX 経由で直接呼出可能にしておくのがアーキテクトの嗜みである。
On Error Resume Next
DX.Register “kernel32.dll”, “GetLastError”, “i=”, “r=l”
‘ 意図的なエラー呼び出し等の直後
Dim apiErrCode
apiErrCode = DX.GetLastError()
If apiErrCode <> 0 Then
‘ システムエラーコードに対応するメッセージの取得などのハンドリング
WScript.Echo “[CRITICAL API ERROR] Win32 Error Code: ” & apiErrCode
Err.Clear
End If
On Error GoTo 0
—
結論:現代に生きるレガシーアーキテクチャの矜持
DynamicWrapperX を導入した VBScript は、もはや単なる「簡易自動化スクリプト」ではない。C言語と同等に Windows のカーネル空間・ユーザー空間と対話し、メモリをミリ単位で制御する極限のシステム制御プログラミング環境へと昇華する。
レガシー環境の保守とは、過去の遺物をそのまま動かし続けることではない。Win32 API という堅牢な基盤に直接アクセスし、ネイティブレベルの精度とパフォーマンスを現代の運用環境に注入することに他ならない。本稿で提示したテクニックを武器に、過酷なミッションを完遂していただきたい。
