在開發 ActiveX/COM 元件供網頁腳本呼叫時,VARIANT 是跨語言資料交換的通用容器。然而,JavaScript (JScript) 與 VBScript 對「陣列」的內部表達截然不同,導致 COM 端接收處理邏輯必須分流。本文深入剖析兩者差異,並提供完整的 ATL/COM 實作代碼,解決大量資料(如 100K 元素)傳遞時的效能瓶頸。
| 特性 | VBScript 陣列 | JavaScript (JScript) 陣列 |
|---|---|---|
| 內部結構 | 標準 SAFEARRAY | 物件集合,非真正陣列 |
| VARIANT 類型 | VT_ARRAY | VT_VARIANT (或具體型別) | VT_DISPATCH (指向 IDispatch) |
| 存取方式 | SafeArrayAccessData 直接記憶體存取 | 需透過 IEnumVARIANT 列舉器逐項取值 |
| 大量資料效能 | 極佳,記憶體連續、一次拷貝 | 極差,逐項 Invoke 呼叫開銷大,CPU/記憶體飆高 |
[
uuid(...),
helpstring("MyComLib")
]
library MyComLib
{
[
uuid(...),
helpstring("MyControl")
]
coclass MyControl
{
[default] interface IMyControl;
};
[
uuid(...),
helpstring("IMyControl Interface"),
dual, oleautomation
]
interface IMyControl : IDispatch
{
[id(1), helpstring("方法 InputArray")]
HRESULT InputArray([in] VARIANT vData);
};
};
<script language="VBScript">
Dim arr(100000)
For i = 0 To 100000 : arr(i) = i Mod 256 : Next
MyControl.InputArray arr ' 直接傳 SAFEARRAY,極快
</script>
<script language="JavaScript">
var arr = new Array(100000);
for (var i = 0; i < 100000; i++) arr[i] = i % 256;
// 直接傳遞會走 VT_DISPATCH 路徑,極慢
MyControl.InputArray(arr);
</script>
建議:大量資料傳遞時,優先使用 VBScript,或改用 VBArray 轉換、Base64 編碼字串、或 ArrayBuffer/Blob 配合自定義介面。
STDMETHODIMP CMyControl::InputArray(VARIANT vData)
{
LPBYTE pBytes = nullptr;
DWORD dwLen = 0;
HRESULT hr = E_FAIL;
if (vData.vt == VT_DISPATCH) {
// JavaScript 陣列 -> VT_DISPATCH
hr = VariantEnumToBytes(vData.pdispVal, &pBytes, &dwLen);
}
else {
// VBScript 陣列 -> VT_ARRAY / VT_VARIANT
hr = VariantArrayToBytes(&vData, &pBytes, &dwLen);
}
if (SUCCEEDED(hr)) {
// TODO: 對 pBytes[0..dwLen-1] 處理業務邏輯
delete[] pBytes;
}
return hr;
}
HRESULT VariantEnumToBytes(IDispatch* pDisp, LPBYTE* ppBytes, DWORD* pdwBytes)
{
CComVariant varResult;
DISPPARAMS noArgs = {0};
// 1. 取得 _NewEnum 屬性 (DISPID_NEWENUM)
HRESULT hr = pDisp->Invoke(DISPID_NEWENUM, IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_PROPERTYGET, &noArgs, &varResult, NULL, NULL);
if (FAILED(hr)) return hr;
// 2. 轉為 IUnknown/IDispatch 介面指標
if (varResult.vt != VT_UNKNOWN && varResult.vt != VT_DISPATCH) return E_FAIL;
CComQIPtr pEnum(varResult.punkVal);
if (!pEnum) return E_FAIL;
// 3. 先數元素個數 (Skip 法)
*pdwBytes = 0;
while (pEnum->Skip(1) == S_OK) (*pdwBytes)++;
// 4. 分配記憶體
*ppBytes = new BYTE[*pdwBytes];
if (!*ppBytes) return E_OUTOFMEMORY;
// 5. 重置列舉器,逐項取值
pEnum->Reset();
CComVariant varElem;
DWORD idx = 0;
while (pEnum->Next(1, &varElem, NULL) == S_OK && idx < *pdwBytes) {
if (varElem.vt == VT_I4 || varElem.vt == VT_UI1) {
(*ppBytes)[idx++] = static_cast(varElem.lVal);
}
else {
varElem.ChangeType(VT_UI1);
(*ppBytes)[idx++] = varElem.bVal;
}
}
return S_OK;
}
HRESULT VariantArrayToBytes(VARIANT* pVar, LPBYTE* ppBytes, DWORD* pdwBytes)
{
// 1. 驗證型別:VT_VARIANT | VT_BYREF -> 指向另一個 VARIANT,該 VARIANT 為 VT_ARRAY
if ((pVar->vt & (VT_VARIANT | VT_BYREF)) != (VT_VARIANT | VT_BYREF)) return E_INVALIDARG;
VARIANT* pInner = pVar->pvarVal;
if (!(pInner->vt & VT_ARRAY)) return E_INVALIDARG;
// 2. 取得 SAFEARRAY 指標
SAFEARRAY* psa = (pInner->vt & VT_BYREF) ? *pInner->pparray : pInner->parray;
if (!psa || SafeArrayGetDim(psa) != 1) return E_INVALIDARG; // 僅支援一維
// 3. 鎖定記憶體並拷貝
VARIANT* pData = nullptr;
HRESULT hr = SafeArrayAccessData(psa, reinterpret_cast(&pData));
if (SUCCEEDED(hr)) {
*pdwBytes = psa->rgsabound[0].cElements;
*ppBytes = new BYTE[*pdwBytes];
for (DWORD i = 0; i < *pdwBytes; ++i) {
CComVariant v = pData[i];
v.ChangeType(VT_UI1); // 統一轉 Byte
(*ppBytes)[i] = v.bVal;
}
SafeArrayUnaccessData(psa);
}
// 注意:此處不銷毀 psa,所有權在呼叫端
return hr;
}
Invoke 會導致秒級延遲,建議改用 VBScript 或 VBArray.toArray() 轉換後再傳(但仍屬 VT_DISPATCH)。btoa(String.fromCharCode.apply(null, uint8Array)) → COM 端 CryptStringToBinary 解碼,單次跨界呼叫,效能提升 100 倍以上。new BYTE[] 對應 delete[],SafeArrayAccessData 必配 SafeArrayUnaccessData,CComVariant 自動清理。E_INVALIDARG、E_OUTOFMEMORY、TYPE_E_TYPEMISMATCH 便於腳本端 try/catch 區分處理。若專案允許使用較新技術棧,建議棄用 ActiveX,改採:
理解 VARIANT 在跨語言邊界的具體表現,是編寫高效能 COM 元件的關鍵。VBScript 的 SAFEARRAY 映射完美契合 COM 模型;JavaScript 的集合物件則需額外列舉開銷。實務上,大量二進位資料傳遞請優先選擇字串編碼或現代 Web API,將 COM 互通層限制在輕量級控制指令上,兼顧相容與效能。