Windows 提供 msscript.ocx (Microsoft Script Control) 組件,允許 C++ 應用程序直接嵌入 JScript/VBScript 引擎,動態執行腳本代碼、調用腳本函數、實現宿主與腳本雙向互操作。本文詳細講解從導入類型庫、創建腳本控件、加載腳本代碼、調用函數、參數封送、結果獲取、資源清理的完整流程。
// 導入 msscript.ocx 生成 .tlh/.tli 文件
#import "msscript.ocx" no_namespace
// 生成的關鍵接口:
// IScriptControl - 腳本控件主接口
// IScriptProcedureCollection - 過程集合
// IScriptProcedure - 單個過程/函數
// 智能指針管理 COM 對象生命週期
CComPtr m_pScriptControl;
// 創建實例
HRESULT hr = m_pScriptControl.CoCreateInstance(__uuidof(ScriptControl));
if (FAILED(hr)) {
// 處理錯誤:組件未註冊、版本不匹配等
return hr;
}
// 設置腳本語言為 JScript
m_pScriptControl->put_Language(L"JScript");
// 禁用 UI 交互(防止腳本彈出對話框阻塞)
m_pScriptControl->put_AllowUI(VARIANT_FALSE);
// 可選:設置超時、啟用/禁用模塊等
// m_pScriptControl->put_Timeout(5000); // 5秒超時
// 添加 JScript 函數定義
// 注意:雙引號在寬字符串中需轉義,或使用單引號
BSTR bstrCode = SysAllocString(L"function test(str1, str2) { return str1 + '-ok-' + str2; }");
m_pScriptControl->AddCode(bstrCode);
SysFreeString(bstrCode);
// 也可加載整個 .js 文件內容
// m_pScriptControl->AddCode(bstrFileContent);
// 獲取所有過程集合
CComPtr pProcColl;
m_pScriptControl->get_Procedures(&pProcColl);
// 獲取過程數量
long nCount = 0;
pProcColl->get_Count(&nCount);
// 關鍵:GetItem 參數為 1-based 索引(非 0-based)
CComPtr pProc;
pProcColl->get_Item(_variant_t(nCount), &pProc); // 獲取最後添加的函數
// 獲取函數名
BSTR bstrName;
pProc->get_Name(&bstrName);
_bstr_t strFuncName(bstrName, false); // 智能管理 BSTR
// JScript 函數參數通過 SAFEARRAY(VARIANT) 傳遞
// 假設 test 函數接受 2 個字符串參數
const int nArgs = 2;
VARIANT vArg; VariantInit(&vArg);
vArg.vt = VT_BSTR;
// 創建一維 SAFEARRAY
SAFEARRAYBOUND bound = { nArgs, 0 }; // 元素數, 下界
SAFEARRAY* psa = SafeArrayCreate(VT_VARIANT, 1, &bound);
if (!psa) return E_OUTOFMEMORY;
// 填充參數
for (long i = 0; i < nArgs; i++) {
vArg.bstrVal = SysAllocString(L"test"); // 參數值
SafeArrayPutElement(psa, &i, &vArg);
// 注意:此處未釋放 vArg.bstrVal,SafeArrayDestroy 會自動處理
}
// Run 方法直接接受函數名和參數數組
_variant_t vResult;
HRESULT hr = m_pScriptControl->Run(strFuncName, &psa, &vResult);
if (SUCCEEDED(hr)) {
// 處理返回值
if (vResult.vt == VT_BSTR) {
_bstr_t bstrResult(vResult.bstrVal, false);
printf("Result: %s\n", (const char*)bstrResult); // 輸出: test-ok-test
}
// 其他類型轉換:VT_I4, VT_R8, VT_BOOL, VT_DISPATCH 等
VariantClear(&vResult);
}
// 手動清理 SAFEARRAY 中的 BSTR(Run 內部可能不會釋放輸入參數)
for (long i = 0; i < nArgs; i++) {
VARIANT v; VariantInit(&v);
SafeArrayGetElement(psa, &i, &v);
if (v.vt == VT_BSTR) SysFreeString(v.bstrVal);
VariantClear(&v);
}
SafeArrayDestroy(psa);
class CJScriptEngine {
CComPtr m_pEngine;
public:
HRESULT Initialize() {
HRESULT hr = m_pEngine.CoCreateInstance(__uuidof(ScriptControl));
if (SUCCEEDED(hr)) {
m_pEngine->put_Language(L"JScript");
m_pEngine->put_AllowUI(VARIANT_FALSE);
}
return hr;
}
HRESULT AddScript(const wchar_t* script) {
CComBSTR bstr(script);
return m_pEngine->AddCode(bstr);
}
HRESULT CallFunction(const wchar_t* funcName, VARIANT* args, int argCount, VARIANT* pResult) {
// 構建 SAFEARRAY
SAFEARRAYBOUND bound = { argCount, 0 };
CComSafeArray sa(1, &bound);
for (int i = 0; i < argCount; i++) {
sa.SetAt(i, args[i]);
}
CComBSTR bstrFunc(funcName);
return m_pEngine->Run(bstrFunc, &sa, pResult);
}
void Uninitialize() { m_pEngine.Release(); }
};
// 使用示例
CJScriptEngine engine;
engine.Initialize();
engine.AddScript(L"function add(a,b) { return a+b; }");
VARIANT args[2];
args[0].vt = VT_I4; args[0].lVal = 10;
args[1].vt = VT_I4; args[1].lVal = 20;
VARIANT result; VariantInit(&result);
engine.CallFunction(L"add", args, 2, &result);
// result.lVal == 30
VariantClear(&result);
engine.Uninitialize();
// 1. 定義 COM 可見接口
__interface IMyObject : IDispatch {
HRESULT DoSomething([in] BSTR msg, [out, retval] long* result);
};
// 2. C++ 類實現接口
class CMyObject : public IDispatchImpl {
STDMETHOD(DoSomething)(BSTR msg, long* result) {
// C++ 實現
*result = 42;
return S_OK;
}
};
// 3. 向腳本引擎添加命名項
CComPtr pObj = new CMyObject();
m_pEngine->AddObject(L"myObj", pObj, TRUE); // TRUE: 允許腳本訪問
// 4. JScript 中調用
// var result = myObj.DoSomething("Hello from JS");
// 捕獲腳本運行時錯誤
// 在 AddCode/Run 後檢查 m_pEngine->get_Error()
CComPtr pError;
m_pEngine->get_Error(&pError);
if (pError) {
long line, col; BSTR text, desc, src;
pError->get_Line(&line);
pError->get_Column(&col);
pError->get_Text(&text);
pError->get_Description(&desc);
pError->get_Source(&src);
// 顯示詳細錯誤信息
}
// 保存腳本引擎狀態(全局變量、函數定義)
IPersistStreamInit* pPersist;
m_pEngine->QueryInterface(IID_IPersistStreamInit, (void**)&pPersist);
IStream* pStream;
CreateStreamOnHGlobal(NULL, TRUE, &pStream);
pPersist->Save(pStream, TRUE);
// 將 pStream 寫入文件/註冊表...
pStream->Release();
pPersist->Release();
// 恢復
pPersist->Load(pStream);
| 問題 | 原因 | 解決 |
|---|---|---|
| CoCreateInstance 失敗 (0x80040154) | msscript.ocx 未註冊 | 管理員運行 regsvr32 msscript.ocx,確認 SysWOW64/System32 下存在 |
| AddCode 後函數不可用 | 語法錯誤、語言不匹配 | 檢查 get_Error,確認 put_Language(L"JScript") |
| Run 返回 DISP_E_EXCEPTION | 腳本運行時拋出異常 | 調用 get_Error 獲取行號、描述 |
| 參數類型不匹配 | VT 類型與 JScript 期望不符 | 數字用 VT_I4/VT_R8,字符串用 VT_BSTR,布爾用 VT_BOOL,對象用 VT_DISPATCH |
| SAFEARRAY 內存洩漏 | 未正確釋放 BSTR/變體 | 使用 CComSafeArray/CComVariant 智能指針,或嚴格 VariantClear/SysFreeString/SafeArrayDestroy |
基於 msscript.ocx 的 JScript 嵌入方案,通過 COM 互操作實現了 C++ 與動態腳本的無縫集成。核心流程:導入類型庫 → 創建 IScriptControl → 設置語言 → AddCode 加載腳本 → get_Procedures/get_Item 獲取函數 → 構建 SAFEARRAY(VARIANT) 參數 → Run 執行 → 處理 _variant_t 結果 → 清理資源。掌握智能指針(CComPtr/CComBSTR/CComSafeArray/CComVariant)管理 COM 對象生命週期,可避免絕大多數內存洩漏與崩潰。進階場景下,雙向互操作、錯誤捕獲、狀態持久化進一步擴展了應用邊界。