eraシリーズ改造/バリアント開発の覚え書き

11月1日の雑記


PACK/UNPACKについて試行錯誤したときのログ。完成版はPACK/UNPACKのページに。

思いついたので


とりあえずまだ頭の中で考えているだけだが、数値型配列をバイナリにPACK/バイナリを数値型配列にUNPACKするような処理を考えている。ネックなのはUNPACKの方なのだが、ひとまずおいておいて、PACKのほう。
PACK処理、つまりバイナリにするわけだが、これは簡単。というか、文字列というのが要はバイナリの配列なわけだから、UNICODE関数で文字に変換して文字列に連結していけばいい。

問題はUNPACKで、これはENCODETOUNI関数を使う。つまり実装はできそうなのだが、何が問題かというと、ひとつは懸念。試してないからなんともいえないのだが、UNICODE関数で作った文字がvalidかどうか分からんので、ちゃんとENCODETOUNIで正しくUNPACKできるのか?という。もっともENCODETOUNIできないとしたらUNICODEの引数が不正なんだろうという感じなので、UNICODEに渡す数値をちゃんとうまく取り扱えるようにしないといけない。このへんはENCODETOUNIの実装も確認した方がいいだろう。
もう一つは、ENCODETOUNIの戻り値はRESULT配列に代入されるというところ。任意の配列に代入しようと思ったら、ループするとかなんとかしないといけない。CASE文中のTOみたいな範囲リテラルがあればいいんだけどなー。LOCAL:0 = RESULT:1 TO RESULT:(RESULT:0)みたいにかけるとめっちゃ楽である。あるいは単にENCODETOUNIがSPLITみたいに引数で代入先を指定できればいいかも。

究極的には参照渡しが欲しいという話になりそうなのだが、もし参照渡しがあるのならPACKもUNPACKもいらなくなる気がするなあ。

試してみた


LOCALS = %UNICODE(0x0025)%%UNICODE(0x0000)%%UNICODE(0x2024)%
ENCODETOUNI %LOCALS%
REPEAT RESULT:0
    PRINTFORM %TOSTR(RESULT:(COUNT+1), "x")% 
REND
PRINTL

ユニコード文字列をENCODETOUNIに渡して一文字ずつPRINTしてみた。ASCIIだろうとなんだろうと1文字として扱っている、というわけで、まあUnicodeだからそりゃあそうだろう。UTF8だったりすると話は違ってくるのだが、ともかくこの辺は大丈夫そうだ。
文字種別の方はというと、ヌル文字も含めて制御記号系も一応大丈夫は大丈夫なようだが、UNICODEの引数に0xD800〜0xDBFFおよび0xDC00〜0xDFFF、つまりサロゲートペアに使われる領域の値が来るとコケる。UNICODE関数はもともと文字列を生成するための関数なのでしょうがないが、この領域を含むバイナリ列を生成することは難しそうだ。困ったことに十進の数値で考えると別に変な値というわけでもなく、55,296〜57,343という、ごくごく普通に使う値だったりするから困る。
しかしながら、そもそもUNICODEの引数は16bit整数値で、0xffff、つまり65,536までの値を渡すことができるのだが、Emueraの整数型は64bit整数値である。UNICODEに渡すためには、もともと16bit単位で4分割する必要があるわけだ。
そこで、16bitの範囲は前述のとおり一部の値が使えないので、だったら8bitずつに分割しちゃったらどうだろう。

LOCALS = 
LOCAL:0 = 1, 32, 4000, 50000, 1000000, 0, 8, 128
REPEAT 8
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT >> 56 & 0xFF)%
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT >> 48 & 0xFF)%
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT >> 40 & 0xFF)%
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT >> 32 & 0xFF)%
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT >> 24 & 0xFF)%
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT >> 16 & 0xFF)%
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT >> 8 & 0xFF)%
    LOCALS = %LOCALS%%UNICODE(LOCAL:COUNT & 0xFF)%
REND
ENCODETOUNI %LOCALS%
REPEAT RESULT:0
    VARSET LOCAL
    LOCAL = RESULT:(++COUNT) << 56
    LOCAL |= RESULT:(++COUNT) << 48
    LOCAL |= RESULT:(++COUNT) << 40
    LOCAL |= RESULT:(++COUNT) << 32
    LOCAL |= RESULT:(++COUNT) << 24
    LOCAL |= RESULT:(++COUNT) << 16
    LOCAL |= RESULT:(++COUNT) << 8
    LOCAL |= RESULT:(COUNT+1)
    PRINTFORM %TOSTR(LOCAL)%, 
REND

力技で書いているのは目をつぶってもらうとして、ちゃんとPACK/UNPACKできている。
しかし毎回こういう処理を書くわけにはいかないから、REPEATの内部でやってる処理くらいはサブルーチンにしてしまったほうがいいだろう。それをPACK/UNPACKという名前の関数にするかどうかというとちょっとイマイチだが。

そもそも何に使うのよ


数値型の配列を文字列で表現することができれば、関数の引数として配列を渡せるようになる。しかしながら、関数の引数は値渡しなのであんまりでかい文字列を渡すのは気がひける。今回の感じだと、PACKしようがデリミタ区切りで文字列連結しようがたいして容量は変わらないという感じだが(むしろ小さな数値の配列なら文字列連結した方がいいだろう)、どんな大きさの数値でも同じ分の領域を確保する今回のPACKの仕方にも問題があるわけで、ここを工夫すればそれなり実用的になる……かもしれない。
PACKするときに、あらかじめルールを決めておくわけだ。最初は8bit整数、次は32bit整数が2回続いて、その次は64bit整数、というように。そうすれば、少なくとも小さい値しか入らないというところに、余分に領域を確保してやる必要はなくなるから、ちょっとは容量を削減できる。UNPACKするときには同じルールでUNPACKすればいい。
実はこのやり方、C言語でいう構造体のようなものである。PACKのルールが構造体定義みたいなものだと思えばいい。
このあたりで、「構造体だったらデータ型が混在してもいいんじゃない?」と考えた人は、なかなかするどい。PACKするときのルールとして「1個目の要素は2byteの整数値、2個目の要素は最大10文字の文字列ですよ」というふうに設定すれば、整数と文字列が混在するデータを取り扱うことができる。まさしく構造体みたいなものを取り扱えるというわけだ。
複雑なデータを構造体もどきにPACKして、関数の引数として渡す。関数の内部でUNPACKして変数に展開する。そこまでの処理を簡潔に記述できたなら、Emueraの引数の取り扱いはもっと便利になるだろう。うまくやれば擬似的なキーワード引数を実現できるし、可変長の引数というのもぐっとやりやすくなる。

もっとも、バイナリのPACK/UNPACKにこだわらずとも、デリミタを使って文字列連結して引数に渡して、関数内でSPLITで分割する、というやり方でも同じことは実現できる。性能に関しては検証してみないとなんともいえないが、PACK/UNPACKの方が有利だ、という感じはあまりない。処理のステップ数が多いし、SPLITの方はC#側で処理するから、SPLITの処理そのものは、そちらの方が有利な気はするし。
どちらかというと単にEmueraでPACK/UNPACKを実現してみようというほうが大きいというかほぼそれが動機で手段と目的が完全に入れ替わっているような気もするが、まあこういうことができるかもしれませんよ、ということで。

SIGNED/UNSIGNED


よく考えてみると今のままだとUNSIGNEDな値しか取り扱えないのだった。SIGNEDを取り扱うためには、マイナスの値を表現する必要がある。
そこで、先頭2バイトをヘッダ領域として扱って、そこに符号情報を格納するというのはどうだろう。
どうせヘッダ領域を設けるなら、サイズも一緒に格納してよさそうだ。符号はマイナスが付くかどうかだけ分かればいいので、1bitで充分。サイズも16bit、32bit、48bit、64bitの4種類しかないから2bitで表現できるだろう。今後文字列型を一緒に格納できるようにしたいということなら、少し考える必要が出てくるが、とりあえず置いておく。

さて、SIGNEDで思い出したが、SIGNED SHORT INTの範囲は-32,767〜32,768である。そういえば、UNICODE関数は32,768までの範囲なら特に問題は起こらないはずだ。だったら、32,768までの数字は1文字目の範囲に収めて、それを超えたら2文字目を使う、というのはどうだろうか。場合によってはコンパクトになるかもしれない。少なくとも32,768を超えない範囲では効果がありそうだ。もちろん32,768を超えているかどうかをどこかに保持しなきゃいけないが、ヘッダ領域にはまだ余裕がある。これはフラグを立てればいいだろう。16bit毎に分割するから、4bitでよさそう。最初の16bitを1bit目に、次を2bit目に、というふうにするわけだ。
ところで、32,768を超えたら2文字目を使うと書いたが、32,768を超えた場合、1文字目は常に32,768である。32,768を超えているかどうかはフラグで持つから、1文字目の32,768はもはや保持する必要がない、超えた分だけを保持しておけばいいことになる。そうすると、64bit整数をヘッダ領域の16bit+データ部16bit*4で表現できるから10byteで済む。前は16byteも必要だったからずいぶん小さく済むようになったと言えるだろう。というわけで実装してみる。

LOCAL:0 = 1, 32, 4000, -50000, 100000, 0, 8, -128
REPEAT 8
    H = 0
    SIF SIGN(LOCAL:COUNT) == -1
    SETBIT H, 7
    SELECTCASE LOCAL:COUNT
    CASE IS <= 0xFFFF
        L = 1
        ; ビットを立てない
    CASE IS <= 0xFFFFFFFF
        L = 2
        SETBIT H, 5
    CASE IS <= 0xFFFFFFFFFFFF
        L = 3
        SETBIT H, 6
    CASEELSE
        L = 4
        SETBIT H, 5
        SETBIT H, 6
    ENDSELECT
    LOCALS:1 = %""%
    COUNT:1 = COUNT
    FOR COUNT, 0, L
        A = ABS(LOCAL:(COUNT:1)) >> (16 * COUNT) & 0xFFFF
        IF A > 0x7FFF
            A &= 0x7FFF
            SETBIT H, COUNT
        ENDIF
        LOCALS:1 = %LOCALS:1%%UNICODE(A)%
    NEXT
    COUNT = COUNT:1
    LOCALS = %LOCALS%%UNICODE(H)%%LOCALS:1%
REND
ENCODETOUNI %LOCALS%
REPEAT RESULT:0
    VARSET LOCAL
    H = RESULT:(++COUNT)
    IF GETBIT(H, 5) && GETBIT(H, 6)
        L = 3
    ELSEIF GETBIT(H, 6)
        L = 2
    ELSEIF GETBIT(H, 5)
        L = 1
    ELSE
        L = 0
    ENDIF
    COUNT:1 = COUNT
    FOR COUNT, 0, L
        IF GETBIT(H, COUNT)
            LOCAL |= (0x8000 + RESULT:(++COUNT:1)) << (16 * COUNT)
        ELSE
            LOCAL |= RESULT:(++COUNT:1) << (16 * COUNT)
        ENDIF
    NEXT
    COUNT = COUNT:1
    IF GETBIT(H, L)
        LOCAL |= 0x8000 + RESULT:(COUNT+1) << (16 * L)
    ELSE
        LOCAL |= RESULT:(COUNT+1) << (16 * L)
    ENDIF
    LOCAL *= GETBIT(H, 7) ? -1 # 1
    PRINTFORM %TOSTR(LOCAL)%, 
REND

実装できた。相変わらず力技だが、そのあたりはおいおいリファクタリングすればいい。処理は大きく複雑になったが、ヘッダに情報を保持できるようになって、柔軟にデータを格納できるようになった。前回よりもサイズも小さくなっている。トレードオフといったところだろう。

ここまで来ればひとまず数値配列とバイナリ列の相互変換ができる。現実的に運用するなら専用のユーティリティ関数を用意する必要があるだろうが、とりあえずはこんな感じで。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

リンク

漠々ト、獏
eramaker/eramaker2の開発元の公式サイト。

Emuera - emurator of eramaker
C#で書かれたeramakerのエミュレータ「Emuera」のプロジェクトページ。

eraシリーズを語るスレ まとめ
eraシリーズ全般のまとめ。バリアント情報、改造情報など。

eratoho まとめ
eramakerのバリアント「eratoho」のまとめ。

era板
eraシリーズについての掲示板。

サブページ

Rubiera
Bitbucket上のRubieraプロジェクトページ。Rubieraのソースコードのダウンロードはここで。

管理人/副管理人のみ編集できます