カテゴリー
Computer Development Programming Secure Coding

PhalconのZephir言語版JavaScript文字列エスケープ

以前にJavaScript文字列のエスケープ関数を紹介しました。試しにPhalconのZephirで書いてみました。

(Last Updated On: 2018/08/13)

以前にJavaScript文字列のエスケープ関数を紹介しました。試しにPhalconのZephirで書いてみました。

利用した環境

利用した環境は以下の通りです。

  • OS: Fedora20 x86_64
  • PHP: Fedora20のPHP5.5パッケージ
  • Phalcon: Fedora19用のPhalconのRPM/SRPMパッケージ のcphalconのソースを2.0.0ブランチに切り替えてビルドした物
  • Zephir: composerからインストールした物

2.0.0ブランチのcphalconのバージョン番号は1.3.0となっているのでバージョンを確認すると1.3.0 ALPHA 1と表示されます。

[yohgaki@dev utils]$ phalcon 

Phalcon DevTools (1.3.0 ALPHA 1)

Available commands:
  commands (alias of: list, enumerate)
  controller (alias of: create-controller)
  model (alias of: create-model)
  all-models (alias of: create-all-models)
  project (alias of: create-project)
  scaffold
  migration
  webtools

 

Zephirのインストール

PHPをビルドする環境が整っているLinuxで、composerからZephirをインストールするのは簡単です。PHPのモジュールがビルドできる環境を作っていない方は、まずPHPをソースコードからビルドできるようにしてから試して下さい。phalconコマンドでプロジェクトを作成します。

$ phalcon project test
$ cd test // プロジェクトディレクトリに移動

そのプロジェクトディレクトリから

$ composer require phalcon/zephir

を実行するだけです。バージョン番号の入力を求められますが、最新版で構わないので”*”を入力すると最新版が”./vendor/bin/zephir”にインストールされます。

 

Zephirモジュールの作り方

Zephirがインストールされたら

$ zephir init utils

としてテスト用のモジュールを初期化します。作成されるディレクトリは以下のような構造になっています。

utils/
   ext/ - Zephirモジュールがビルドされる場所
   utils/ - Zephirソースコードの場所

後はutils/utils/以下のディレクトリにクラスを定義したzepファイルを作成し、utilsディレクトリに移動した後、ビルドコマンドを実行するだけです。(別のディレクトリからだとエラーになります)

[yohgaki@dev test]$ cd utils
[yohgaki@dev utils]$ ../vendor/phalcon/zephir/bin/zephir build
Compiling...
Installing...
Extension installed!
Add extension=utils.so to your php.ini
Don't forget to restart your web server

これで(Phalconプロジェクトルート)/utils/ext以下にPHPのCモジュールが作成されます。実際にロードするべき.soファイルは(Phalconプロジェクトルート)/utils/ext/modulesに配置されています。

 

ビルドしたモジュールの使い方

ZephirでビルドしたモジュールはPhalconモジュールがロードされていれば、普通のモジュールのように利用できます。

[yohgaki@dev test]$ php -d "extension=/home/yohgaki/tmp/test-zephir/test/utils/ext/modules/utils.so" -m | grep utils
utils

ビルドしたutilsモジュールがロードされている事が確認できます。

 

JavaScript文字列エスケープ関数

元のPHPコードは以下のコードです。

<?php
function escape_javascript_string($str) {
  $map = [
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,0,0, // 49
          0,0,0,0,0,0,0,0,1,1,
          1,1,1,1,1,0,0,0,0,0,
          0,0,0,0,0,0,0,0,0,0,
          0,0,0,0,0,0,0,0,0,0,
          0,1,1,1,1,1,1,0,0,0, // 99
          0,0,0,0,0,0,0,0,0,0,
          0,0,0,0,0,0,0,0,0,0,
          0,0,0,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1, // 149
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1, // 199
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1, // 249
          1,1,1,1,1,1,1, // 255
          ];
  // 文字エンコーディングはUTF-8
  $mblen = mb_strlen($str, 'UTF-8');
  $utf32 = bin2hex(mb_convert_encoding($str, 'UTF-32', 'UTF-8'));
  for ($i=0, $encoded=''; $i < $mblen; $i++) {
      $u = substr($utf32, $i*8, 8);
      $v = base_convert($u, 16, 10);
      if ($v < 256 && $map[$v]) {
        $encoded .= '\\x'.substr($u, 6,2);
      } else if ($v == 2028) {
        $encoded .= '\\u2028';
      } else if ($v == 2029) {
        $encoded .= '\\u2029';
      } else {
        $encoded .= mb_convert_encoding(hex2bin($u), 'UTF-8', 'UTF-32');
      }
   }
   return $encoded;
}

 

Zephir版は以下のコードです。これは(Phalconプロジェクトルート)/utils/utils/MyEscape.zepとして配置されています。ファイル名とクラス名は一致している必要があります。一致しない場合はエラーになります。

namespace Utils;

class MyEscape
{
    public static function javascript_string(string str) {
        var map, u, c, mblen, utf32, convmap, encoded;
        bool converted;
        int i;
        let map = [
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,0,0, // 49
            0,0,0,0,0,0,0,0,1,1,
            1,1,1,1,1,0,0,0,0,0,
            0,0,0,0,0,0,0,0,0,0,
            0,0,0,0,0,0,0,0,0,0,
            0,1,1,1,1,1,1,0,0,0, // 99
            0,0,0,0,0,0,0,0,0,0,
            0,0,0,0,0,0,0,0,0,0,
            0,0,0,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1, // 149
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1, // 199
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1, // 249
            1,1,1,1,1,1,1 // 255
        ];

        // 文字エンコーディングはUTF-8
        let mblen = mb_strlen(str, "UTF-8");
        let utf32 = bin2hex(mb_convert_encoding(str, "UTF-32", "UTF-8"));
        let convmap = [ 0x0, 0xffffff, 0, 0xffffff ];
        let i = 0;
        let encoded = "";
        while (i < mblen) {
            let u = substr(utf32, i*8, 8);
            let c = base_convert(u, 16, 10);
            let converted = false;
            if (c < 256 && map[c]) {
                let encoded .= "\\x".substr(u, 6, 2);
                let converted = true;
            } else {
                let converted = false;
            }
            if (c == 2028) {
                let encoded .= "\\u2028";
                let converted = true;
            }
            if (c == 2029) {
                let encoded .= "\\u2029";
                let converted = true;
            }
            if (!converted) {
                let encoded .= mb_decode_numericentity("&#".c.";", convmap, "UTF-8");
            }
            let i++;
        }
        return encoded;
    }
}

ZephirはコードをPhalcon用のCモジュールに変換するのでPHPとは多少異なった仕様になっていますが、PHPの関数などは直接呼び出している事が分かります。

C言語と同じ様に変数は使用する前にvar(Variant型)int, boolなどと宣言する必要があります。変数に代入する場合、letが必要です。

if else if elseがおかしな感じになっていますが、まだelse ifがサポートされていないのでこのような書き方になっています。GitHubを見るとelse ifサポートが提案されているので、そのうちサポートされると思います。

forループがwhileループに変わっているのはZephirのforはイテレーション専用になっていて、カウンター変数を使ったループができない為です。forはwhileで書き換えられるので特に大きな問題にはならないでしょう。

より効率的なビットシフトを使用したPHPコードを元のコードにしなかった理由は、Zephirはメモリ保護のため文字列型の変数を配列としてオフセットでアクセスできないようになっているからです。substrを利用しているので効率的とは言えませんし、他にも色々最適化できる部分もありますが比較しやすいようにできるだけ元のPHPコード同じにするため、あまり変更していません。

細かい違いが色々ありますが、PHPプログラマなら比較的簡単にZephirでプログラミングする事が可能だと思います。Zephirの詳しい仕様はマニュアルを参照してください。

 

ベンチマーク

ソースコードからあまり速くならない事が予想できます。このコードには複雑なロジックがなくPHP関数を呼び出す部分以外がネイティブコードに変換されるだけだからです。mbstring関数や文字列関数など処理時間が多い部分はそのままであることが原因です。

phpコマンドの-dオプションで直接モジュールをロードし、約1000文字の文字列を1000回エスケープした結果は以下の通りです。エスケープ部分をmicrotime関数で経過時間を測っています。この為、バイトコードキャッシュの有無はベンチマークには影響ありません。

  • PHP版 4.0071728229523
  • Zephir版 3.6831591129303

1割弱高速化されている事が分かります。複雑なロジックなどZephirモジュール化するとより良い結果を期待できます。このコードはZephirの利用による高速化の実験にはあまり適切とは言えないので参考程度の結果だと思って下さい。

 

追記

少し時間があったのでもう少し効率的なコードも書いてみました。128から255をエスケープしていないので反則と言えますが参考までに。

namespace Utils;

class Escaper {

    static public function js(string str) {
        var encoded, map;
        char ch;
        int cnt, c;

        let map = [
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,0,0, // 49
            0,0,0,0,0,0,0,0,1,1,
            1,1,1,1,1,0,0,0,0,0,
            0,0,0,0,0,0,0,0,0,0,
            0,0,0,0,0,0,0,0,0,0,
            0,1,1,1,1,1,1,0,0,0, // 99
            0,0,0,0,0,0,0,0,0,0,
            0,0,0,0,0,0,0,0,0,0,
            0,0,0,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1, // 149
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1, // 199
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1,
            1,1,1,1,1,1,1,1,1,1, // 249
            1,1,1,1,1,1,1 // 255
        ];
        let encoded = "";
        let cnt=0;
        for ch in str {
                let c = ch;
                if (ch & 0x80) {
                    let encoded .= chr(c);
                    continue;
                } else {
                    // 128 or less
                    if (map[c]) {
                        if (c < 0x10) {
                            let encoded .= sprintf("\\x0%x", c);
                        } else {
                            let encoded .= sprintf("\\x%x", c);
                        }
                    } else {
                        if (c == 2028) {
                            let encoded .= "\\u2028";
                            continue;
                        }
                        if (c == 2029) {
                            let encoded .= "\\u2029";
                            continue;
                        }
                        let encoded .= chr(c);
                    }
                    continue;
                }
            }
        return encoded;
    }
}

同じベンチマークをした結果は以下の通りです。

0.99092888832092

“else if”が無いこと、安全性の為とはいえ文字列を文字(char)として取り扱えない制限はC言語の文字列のように扱えないのでかなり厳しい感じです。少なくとも私にとってはCでモジュールを書いたほうが簡単で速い&早いです。Cが書ける方は少し複雑な文字列操作をするなら普通のCモジュールを書いたほうが良いかも知れません。

しかし、Cモジュールを書こうとするとモジュールの書き方やハッシュなどのAPIの使い方を習得しなければなりません。Zephirの場合、これらの知識が必要ないので適材適所で使うと良いと思います。