イネマルのプログラミング備忘録

趣味プログラマのメモ

WSH JScript Chakra エンジンの共存による機能制限の回避(WScript Quit 代替)

はじめに

WSH JScriptChakra を指定すると WScript Object の機能が制限される為、
WScript.Quit 関数が利用できず、戻り値を返す術が失われます。
今回は、この問題の対処例を挙げます。

前提

Windows 環境には、Windows Script Host(WSH)と呼ばれる
Node.js の様な JavaScript が動作するスクリプトの実行環境が標準搭載されています。

技術としては古いものなので、デフォルトでは ES3 程度のサポートですが、
スクリプトエンジンを Chakra に変更することでES6の機能が利用可能です。
WSH JScript Chakra を使用した ES2015(ES6) 対応 ( スクリプトエンジン まとめ )

JScriptChakra を共存させる

既定のJScript で、Chakra 用の処理をラップする形の対処法です。
WScript.Quit は、CScript でないと戻り値が機能しないので、バッチに埋め込みます。

この方法は、JScriptChakra で各エンジンが共存するので、
機能制限の掛かる箇所は、JScript側で関数化する等の対処で回避可能になります。
※ 例えば、GetObject等のChakraでは未定義となる関数は、JScript 経由で利用できます。

0</* ::
@cscript /nologo /E:JScript "%~f0" %*

@rem 戻り値の確認用
@echo.ExitCode:%errorlevel%&pause

@exit /b %errorlevel%&*/0;//@cc_on @if(0)
//
// Chakra
//

// エントリーポイント用の関数(プロセスの戻り値を返す)
function main()
{
    // コンソールに表示
    const msg = `${WSH} for Chakra`;
    WSH.Echo(msg);
    
    // JScript側の関数を呼ぶ
    const r = JS.hoge();
    
    // 強制終了するなら、例外を投げる
    //throw "quit";
    // もしくは、早期リターン
    //return;
    
    // ポップアップを表示
    // ActiveXObject は未定義なので、CreateObject を使う
    const shell = WSH.CreateObject("WScript.Shell");
    shell.popup(msg);
    
    // 戻り値
    return r;
}

function fuga()
{
    WSH.Echo("fuga");
}

var quit=!1;let item,ie,w=WSH.CreateObject("Shell.Application").Windows(),i=0;
for(;i<w.Count;i++)if((item=w(i))&&item.hWnd==WSH.Arguments(0)){ie=item;break}
for(ie.PutProperty("@",this);!quit;)WSH.Sleep(1e3);/*@end @cc_on
for(var ckr,ie=WSH.CreateObject("InternetExplorer.Application"),sh=WSH.CreateObject("WScript.Shell"),
p=sh.Exec('cscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "'+WSH.ScriptFullName+'" '+ie.hWnd);!(ckr=ie.GetProperty("@"));)
{if(0!=p.Status){for(;!p.StdErr.AtEndOfStream;)WSH.StdErr.Write(p.StdErr.Read(256));ie.Quit(),WSH.Quit(p.ExitCode)}WSH.Sleep(1e3)}
(this.CKR=ckr).JS=this,ckr.WScript=WSH,ckr.WSH=WSH,ie.Quit(),r=CKR.main(),ckr.quit=!0,WSH.Quit(r);
//
// JScript
//

function hoge()
{
   // Chakra側の関数を呼ぶ
   CKR.fuga();
   
   return 123;
}

@*/

仕組み

条件付きコンパイル 機能を使って、1つのスクリプト内に
エンジン毎(JScript, Chakra)で実行されるコードを共存させています。

また、プロセス間でオブジェクトを共有するために、
InternetExplorer Object の PutProperty / GetProperty を利用しています。

該当部分は、コーディングに直接関係無いのでMinify済みですが、
大体下記のように処理しています。

  • JScript
    1. IEオブジェクトを作成
    2. 実行引数に、IEのウィンドウハンドルを設定して、Chakraエンジンで自身を起動
    3. GetPropertyでChakraのオブジェクトが設定されるまで待機
    4. ChakraのWScriptを、JScriptの定義で上書き
    5. Chakra側のmain関数を呼び出して、戻り値を受け取る
    6. プロセス同期用のフラグを折ってChakra側を終了
    7. main関数の戻り値をWScript.Quitに指定して終了
  • Chakra
    1. 実行引数に渡されたウィンドウハンドルから、IEオブジェクトを特定
    2. PutPropertyで、IEオブジェクトに this を設定
    3. プロセス同期用のフラグが折れるまで待機

追記(おそらく最適解):例外を握りつぶすと戻り値が使えるらしい

2022/05/28:
例外をキャッチすれば正しく戻り値が扱えるという話を見つけました。
実際試してみると、普通に扱えているようなので、これが最適解でしょう。
WSHでES2015を利用するベストプラクティス - Qiita

戻り値を確認するため、exit /bで戻り値を設定するバッチに、スクリプトを埋め込みます。

0</* ::
@%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*
@echo 戻り値は、%errorlevel%です。&pause
@exit /b %errorlevel%&*/0;

// エントリ
let global_result=(()=>{
    
    // ポップアップを表示
    // ActiveXObject は未定義なので、CreateObject を使う
    const shell = WSH.CreateObject("WScript.Shell");
    shell.popup(`${WSH} for Chakra`);
    
    return 123;
})();

try {WScript.Quit(global_result);
}catch(e){// JavaScript runtime error: Object doesn't support this action
}

おわりに

一般的に、外部から呼び出されることを想定するプログラムは、
戻り値から、エラー原因を特定できるようにしています。

Chakra を指定したスクリプトでは、スクリプトが失敗しても
戻り値から判断できない状況でしたが、これで一件落着?でしょうか。

元々 Chakra を指定する事自体が、仕様の穴をつくような方法なので、
回りくどい対応を取らざる得なくなる事もありますが、
それを差し引いても ES6 が利用できる点は大きいかと思います。

参考