Tamarin の avmplus.exe に独自関数を実装

Tamarin と遊んでみた」の続きです。

今回は、avmplus.exe にビルトイン関数を追加してみます。

// hoge.as
import avmplus.*;
System.alert("hoge");

avmplus.System パッケージには exit などの関数がありますが、alert 関数は実装ありません。そのため、この hoge.as を実行すると

$ avmplus.exe hoge.abc
ReferenceError: Error #1065

参照エラーになってしまいます。

これを動かすのが今回の目標です。

toplevel.as の実装

toplevel.as を覗いてみましょう。

// toplevel.as
package avmplus 
{
    public class System
    {
        public native static function exit(status:int):void
        public native static function exec(command:String):int
        public native static function getAvmplusVersion():String
        public native static function trace(a:Array):void
        public native static function write(s:String):void
        public native static function debugger():void
        public native static function isDebugger():Boolean
        public native static function getTimer():uint
        private native static function getArgv():Array
        public static const argv:Array = getArgv();
        public native static function readLine():String
    }
// ...

avmplus.System パッケージの関数が宣言されていますね。

native キーワードがついていますが、これらの関数が C++ で関数が実装されていることをあらわします。

alert 関数も native で実装してみたいところですが、まずは ActionScript で実装してみましょう。


ということで、toplevel.as を書き換えます。System.write 関数をそのまんま使っちゃいます。

// toplevel.as
package avmplus 
{
    public class System
    {
        public static function alert(text:String):void
        {
            System.write(text);
        }
        public native static function exec(command:String):int
        public native static function getAvmplusVersion():String
// ...

toplevel.as のコンパイル

さて、avmplus 側は、どう修正すればよいのでしょうか。

答えは、簡単。shell/toplevel.cpp の中にあります。

/* shell/toplevel.cpp */
const int toplevel_abc_length = 4403;
const int toplevel_abc_method_count = 137;
const int toplevel_abc_class_count = 15;
const int toplevel_abc_script_count = 13;
static unsigned char toplevel_abc_data[4403] = {
0x10, 0x00, 0x2e, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff, 0xef, 0x41,
0x86, 0x01, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x0d, 0x74, 0x6f, 0x70, 0x6c,
0x65, 0x76, 0x65, 0x6c, 0x2e, 0x61, 0x73, 0x24, 0x31, 0x00, 0x07, 0x61, 0x76, 0x6d, 0x70, 0x6c,
// ...

この16進数の羅列が toplevel.as のバイトコードなんです。toplevel.as をバイトコードに変換して、toplevel.cpp を作れば動きそうです。

ということで、toplevel.as をコンパイルしてみます。-in を指定して、他の AS もまとめてコンパイルするのを忘れずに。

java -jar asc_authoring.jar ^
    -import global.abc ^
    -in shell\ByteArray.as ^
    -in shell\Domain.as ^
    -in shell\DoubleArray.as ^
    -in shell\Endian.as ^
    -in shell\FloatArray.as ^
    -in shell\IntArray.as ^
    -in shell\Java.as ^
    -in shell\ShortArray.as ^
    -in shell\StringBuilder.as ^
    -in shell\UIntArray.as ^
    -in shell\UShortArray.as ^
    -in extensions\Dictionary.as ^
    shell\toplevel.as

toplevel.abc, 3850 bytes written

toplevel.abc が生成されました。

さらに、なんと toplevel.cpp と toplevel.h も自動的に更新されました。

const int toplevel_abc_length = 3908;
const int toplevel_abc_method_count = 122;
const int toplevel_abc_class_count = 15;
const int toplevel_abc_script_count = 1;
static unsigned char toplevel_abc_data[3908] = {
0x10, 0x00, 0x2e, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff, 0xef, 0x41,
0x79, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x0d, 0x74, 0x6f, 0x70, 0x6c, 0x65,
0x76, 0x65, 0x6c, 0x2e, 0x61, 0x73, 0x24, 0x31, 0x00, 0x07, 0x61, 0x76, 0x6d, 0x70, 0x6c, 0x75,
// ...

どうも、ActionScript 内に native 宣言された関数があると、cpp と h が書き出されるようです。

toplevel_abc_class_count と toplevel_abc_script_count の値が減ってしまっていますが、ひとまずは気にせずに先に進むことにします。

いざ、実行

toplevel.cpp と toplevel.h が更新されたところで、avmplus.exe をビルドします。

いざ、実行!

$ avmplus.exe hoge.abc
hoge

やったー!

地味ですが、System.alert() が実行できたことが分かります。

alert 関数を native 実装

さて、ActionScript で実装していた alert 関数を C++ での実装に切り替えてみましょう。

まずは、toplevel.as の編集です。alert 関数を native 宣言にします。

// toplevel.as
package avmplus 
{
    public class System
    {
        public native static function alert(text:String):void
        public native static function exec(command:String):int
        public native static function getAvmplusVersion():String
// ...

toplevel.as をバイトコードに変換すると、toplevel.cpp と toplevel.h が更新されます。

native 部分の実装は System.exit 関数を参考に、shell/SystemClass.cpp と shell/SystemClass.h を修正してみます。

/* shell/SystemClass.h */
// ...
namespace avmshell
{
    class SystemClass : public ClassClosure
    {
        
        //#define PERFORMANCE_GETTIMER
        #ifdef PERFORMANCE_GETTIMER
        uint64 initialTime;
        #else
        double initialTime;
        #endif

    public:
        SystemClass(VTable* cvtable);
        ~SystemClass();

        // set by shell
        static int user_argc;
        static char **user_argv;

        void alert(Stringp text);

        /**
         * Implementation of System.exit
         * AS usage: System.exit(status);
         * Exits the VM with OS exit code specified by  status.
         */
        void exit(int status);
// ...
/* shell/SystemClass.cpp */
#include "avmshell.h"

#include <stdlib.h>

namespace avmshell
{
    BEGIN_NATIVE_MAP(SystemClass)
        sNATIVE_METHOD(avmplus_System_alert, SystemClass::alert)
        NATIVE_METHOD(avmplus_System_exit, SystemClass::exit)
        // ...
    END_NATIVE_MAP()

    void SystemClass::alert(Stringp text)
    {
        MessageBoxW(NULL, text->c_str(), L"avmplus", MB_ICONEXCLAMATION);
    }

    void SystemClass::exit(int status)
    {
        ::exit(status);
    }
// ...

avmplus_System_alert は、先ほどの toplevel.as のコンパイル時に toplevel.h に生成されています。バイトコード中の native 宣言と C++ の中継ぎをするのが、この BEGIN_NATIVE_MAP であり、avmplus_System_alert であるわけです。

SystemClass::alert では、C++ らしく MessageBoxW 関数を呼び出しています。Win32 プログラミングではおなじみのメッセージ表示用の関数です。W は Unicode 版をあらわします。

いざ、実行 (2)

avmplus.exe をビルドして、hoge.abc を実行してみます。

$ avmplus.exe hoge.abc

メッセージが表示されました。

hoge.abc の再コンパイルしなくても、avmplus.exe の実装を変えるだけで挙動が変わるのがおもしろいですね!