PHPエクステンションをRustで作る

アップデート:この記事の第一稿を投稿してから数時間後、PHPのベンチマークが機能していないことに気付きました。ですから万全を期すため、PHPとRustの双方のバージョンを更新しました。変更箇所は、GitHub(最後にリンクあり)にまとめて載せてあります。

昨年10月、私はEtsyとある同僚と、どうやったらPHPやRuby、Pythonといったインタプリタ型言語で今よりずっと簡単にエクステンションが書けるかについて議論しました。うまく書けない原因の1つとして、エクステンションは概してCで書かれますが、Cに長けているのでなければ自信を持ってコードを書くのは難しい、といったことなどを話しました。

あれ以来、私はずっとRustでエクステンションを書く方法を模索し、ここ数日は実際にあれこれ試してみました。そして今朝、ついにうまくいったのです。

Cに埋め込んだRustをPHPで実行

私の基本的なアイデアは、ライブラリ形式にコンパイルしたRustのコードをいくつか書き、そこにCのヘッダをつけ、Cでそれを用いることでPHPから呼び出せるエクステンションを作る、というものでした。世界一簡単な方法というわけではありませんが、面白そうだと感じました。

Rust FFI

私が最初に行ったのは、RustにCと互換性を持たせることができるRust Foreign Function Interface(FFI)をいじることでした。1つだけの引数(Cのchar型、他の言語ではstring型と呼ばれるデータ型のポインタ)を持つhello_from_rustという1つのメソッドを使って簡単なライブラリを書きました。このメソッドは、入力したものの前に“Hello form Rust,”を加えて出力します。

// hello_from_rust.rs
#![crate_type = "staticlib"]

#![feature(libc)]
extern crate libc;
use std::ffi::CStr;

#[no_mangle]
pub extern "C" fn hello_from_rust(name: *const libc::c_char) {
    let buf_name = unsafe { CStr::from_ptr(name).to_bytes() };
    let str_name = String::from_utf8(buf_name.to_vec()).unwrap();
    let c_name   = format!("Hello from Rust, {}", str_name);
    println!("{}", c_name);
}// hello_from_rust.rs
#![crate_type = "staticlib"]

#![feature(libc)]
extern crate libc;
use std::ffi::CStr;

#[no_mangle]
pub extern "C" fn hello_from_rust(name: *const libc::c_char) {
    let buf_name = unsafe { CStr::from_ptr(name).to_bytes() };
    let str_name = String::from_utf8(buf_name.to_vec()).unwrap();
    let c_name   = format!("Hello from Rust, {}", str_name);
    println!("{}", c_name);
}

このコードの大部分は、『Calling a Rust library from C (or anything else!)』を参考にしました。ここで起こっていることを、うまく説明しています。

これをコンパイルすると、.aのファイルであるlibhello_from_rust.aが得られます。これはスタティックライブラリで、自身の依存関係は全てこの中に含まれています。そして次に行いますが、Cのプログラムをコンパイルする時、そこにリンクすることができます。これをコンパイルすると次のように出力されることに注目してください。

note: link against the following native artifacts when linking against this static library
note: the order and any duplication can be significant on some platforms, and so may need to be preserved
note: library: System
note: library: pthread
note: library: c
note: library: m

これはRustのコンパイラで、この依存関係を使う時に他に何とリンクする必要があるかを教えてくれます。

CからRustを呼び出す

ライブラリができたので、次はこれをCから呼び出せるようにするために、2つのことを行わなくてはなりません。まずは、Cのヘッダファイルhello_from_rust.hを作り、コンパイルする時にリンクさせる必要があります。

ヘッダファイルは次のようになります。

// hello_from_rust.h
#ifndef __HELLO
#define __HELLO

void hello_from_rust(const char *name);

#endif

これはかなり基本的なヘッダファイルであり、1つの関数についてのみシグネチャと定義を提供します。そして、次に書く必要があるのはCのプログラムコードです。

// hello.c
#include <stdio.h>
#include <stdlib.h>
#include "hello_from_rust.h"

int main(int argc, char *argv[]) {
    hello_from_rust("Jared!");
}

そしてコンパイルを実行します。

gcc -Wall -o hello_c hello.c -L /Users/jmcfarland/code/rust/php-hello-rust -lhello_from_rust -lSystem -lpthread -lc -lm

文末に-lSystem -lpthread -lc -lmと付けます。そうやってRustコンパイラでRustのライブラリをコンパイルした際に生じる”ネイティブアーティファクト”に対して、それぞれリンクするようにgccに指示を出しています。

生成されるバイナリを一度実行してしまえば、hello_cは次のように実行できます。

$ ./hello_c
Hello from Rust, Jared!

素晴らしい!CからRustのライブラリを呼び出せました。そこで次の課題は、これをどうやってPHPエクステンションに組み込むかです。

PHPからCを呼び出す

この問題を解決するのにはかなり時間を費やしました。というのも、PHPエクステンションについてのマニュアルがあまり優れていないのです。ただ、PHPソースにext_skel(おそらく”エクステンションスケルトン”の意味)というスクリプトが既に入っていることには助かりました。このスクリプトを使えば、必要なボイラープレートコードをほとんど生成することができます。これを実装するために私はPHPマニュアルの『Extension structure』をかなり読み込みました。

まず手始めにPHPソースをダウンロード、解凍し、そしてPHPディレクトリに移動して実行してみましょう。

$ cd ext/
$ ./ext_skel --extname=hello_from_rust

このようにしてPHPエクステンションを作成するために必要になる基本的なスケルトンが生成されます。そしてこのフォルダを、エクステンションを格納したいローカルディレクトリに移し、以下の

  • rust source
  • rust library
  • c header

を同じディレクトリに移動させます。そうすることで、最終的にディレクトリの中身はこのようになります。

.
├── CREDITS
├── EXPERIMENTAL
├── config.m4
├── config.w32
├── hello_from_rust.c
├── hello_from_rust.h
├── hello_from_rust.php
├── hello_from_rust.rs
├── libhello_from_rust.a
├── php_hello_from_rust.h
└── tests
    └── 001.phpt

1 directory, 11 files

これらのほとんどのファイルについてはPHPマニュアルの『Files which make up an extension』で分かりやすく解説してあります。それでは、config.m4の編集から始めていきます。

コメントは全て省略していますが、こちらが私の組んだコードになります。

PHP_ARG_WITH(hello_from_rust, for hello_from_rust support,
[  --with-hello_from_rust             Include hello_from_rust support])

if test "$PHP_HELLO_FROM_RUST" != "no"; then
  PHP_SUBST(HELLO_FROM_RUST_SHARED_LIBADD)

  PHP_ADD_LIBRARY_WITH_PATH(hello_from_rust, ., HELLO_FROM_RUST_SHARED_LIBADD)

  PHP_NEW_EXTENSION(hello_from_rust, hello_from_rust.c, $ext_shared)
fi

分かったことは、これらは基本的にマクロだということです。これらのマクロについてのマニュアルはかなりひどかったです(例えば、Googleで”PHP_ADD_LIBRARY_WITH_PATH”と検索してもPHPチームが書いた文章が最初の検索結果ページに出てきません)。私はPHPエクステンションにスタティックライブラリをリンクさせる方法について話している古いスレッドを見つけ、PHP_ADD_LIBRARY_PATHのマクロに辿り着きました。それ以外のマクロについてはext_skelを実行した際に生成されるコメントが勧めていたものを使用しています。

ビルド設定をセットアップできたので、次はPHPスクリプトを使ってライブラリに呼び出す必要があります。そうするために、自動的に生成されるhello_from_rust.cというファイルを修正していきます。まず、hello_from_rust.hというヘッダファイルをインクルードするようにし、confirm_hello_from_rust_compiledというPHPメソッドの定義を修正します。

#include "hello_from_rust.h"

// a bunch of comments and code removed...

PHP_FUNCTION(confirm_hello_from_rust_compiled)
{
    char *arg = NULL;
    int arg_len, len;
    char *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    hello_from_rust("Jared (from PHP!!)!");

    len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "hello_from_rust", arg);
    RETURN_STRINGL(strg, len, 0);
}

hello_from_rust("Jared (fromPHP!!)!");をここに挿入しました。

これで、PHPエクステンションの構築に取り掛かれます。

$ phpize
$ ./configure
$ sudo make install

たったのこれだけです!この3行のコードで、まずメタコンフィグを生成し、Makeという設定コマンドを実行し、そしてエクステンションをインストールします。私の場合はここでsudoをインストールに使う必要がありました。なぜなら私のユーザは、直接PHPエクステンションをインストールできる権限を持っている訳ではないからです。

さあ、実行してみましょう! 

$ php hello_from_rust.php
Functions available in the test extension:
confirm_hello_from_rust_compiled

Hello from Rust, Jared (from PHP!!)!
Congratulations! You have successfully modified ext/hello_from_rust/config.m4. Module hello_from_rust is now compiled into PHP.
Segmentation fault: 11

成功です!PHPは確かにCで作ったエクステンションにアクセスし、利用可能なメソッドや呼び出しのリストを確認しています。そしてこのCエクステンションがRustのライブラリにアクセスし、私たちが最初に作った文字列をPHPに出力しました。すばらしいです!しかし最後にセグメンテーション違反が表示されていますが、これはどういうことでしょう? 

私に分かる限りでは、この問題はRustのprintln!というマクロを使ったことに関係しているようですが、現時点ではまだ掘り下げて確認していません。でもこのマクロを削除し、代わりにRustライブラリからchar*を返すように変更すれば、セグメンテーション違反は無くなります。

こちらがそのRustのコードです。

#![crate_type = "staticlib"]

#![feature(libc)]
extern crate libc;
use std::ffi::{CStr, CString};

#[no_mangle]
pub extern "C" fn hello_from_rust(name: *const libc::c_char) -> *const libc::c_char {
    let buf_name = unsafe { CStr::from_ptr(name).to_bytes() };
    let str_name = String::from_utf8(buf_name.to_vec()).unwrap();
    let c_name   = format!("Hello from Rust, {}", str_name);

    CString::new(c_name).unwrap().as_ptr()
}

そして、Cヘッダを次のように変更します。

#ifndef __HELLO
#define __HELLO

const char * hello_from_rust(const char *name);

#endif

これに伴い、Cエクステンションも次のように変わります。

PHP_FUNCTION(confirm_hello_from_rust_compiled)
{
    char *arg = NULL;
    int arg_len, len;
    char *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    char *str;
    str = hello_from_rust("Jared (from PHP!!)!");
    printf("%s\n", str);

    len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "hello_from_rust", arg);
    RETURN_STRINGL(strg, len, 0);
}

役に立たないマイクロベンチマーク

さて、なぜこんな方法を使う必要があるのでしょうか?正直に言いますと私はまだ、実世界でこの方法が必要になるケースに遭遇したことはありません。でも、PHPエクステンションがまさに適切な事例は思い当たります。フィボナッチ数列に関するアルゴリズムです。これは大体の場合、かなり簡潔で分かりやすいアルゴリズムです(以下、Rubyによる例です)。

def fib(at) do
    if (at == 1 || at == 0)
        return at
    else
        return fib(at - 1) + fib(at - 2)
    end
end

性能に関しては、本当にひどいものになりますが、再帰を使わなければパフォーマンスは向上します。

def fib(at) do
    if (at == 1 || at == 0)
        return at
    elsif (val = @cache[at]).present?
        return val  
    end

    total  = 1
    parent = 1
    gp     = 1

    (1..at).each do |i|
        total  = parent + gp
        gp     = parent
        parent = total
    end

    return total
end

では1つはPHPだけで、もう1つはRustも使って2つの例文を書き、どちらが速いか比べてみましょう。こちらがPHPだけのバージョンです。

<?php

function fib($at) {
    if ($at == 0 || $at == 1) {
        return $at;
    } else {
        $total  = 1;
        $parent = 1;
        $gp     = 0;

        for ($i = 1; $i < $at; $i++) {
            $total  = $parent + $gp;
            $gp     = $parent;
            $parent = $total;
        }

        return $total;
    }
}

for ($i = 0; $i < 100000; $i ++) {
    fib(92);
}

こちらが実行時間の結果です。

$ time php php_fib.php

real    0m2.046s
user    0m1.823s
sys 0m0.207s

Rustを使ったバージョンでも試しましょう。こちらがライブラリのソースです。

#![crate_type = "staticlib"]

fn fib(at: usize) -> usize {
    if at == 0 {
        return 0;
    } else if at == 1 {
        return 1;
    }

    let mut total  = 1;
    let mut parent = 1;
    let mut gp     = 0;
    for _ in 1 .. at {
        total  = parent + gp;
        gp     = parent;
        parent = total;
    }

    return total;
}

#[no_mangle]
pub extern "C" fn rust_fib(at: usize) -> usize {
    fib(at)
}

コードを軽く確認してみましょう。私はrustc -O rust_lib.rsを使って、このライブラリを最適化できるようにコンパイルしました。(パフォーマンスをベンチマーキングするためにです)。こちらがCエクステンションのソースです。(関連する部分のみ抜粋しました)。

PHP_FUNCTION(confirm_rust_fib_compiled)
{
    long number;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &number) == FAILURE) {
        return;
    }

    RETURN_LONG(rust_fib(number));
}

実行に必要なPHPのスクリプトは以下の通りです。

<?php
$br = (php_sapi_name() == "cli")? "":"<br>";

if(!extension_loaded('rust_fib')) {
    dl('rust_fib.' . PHP_SHLIB_SUFFIX);
}

for ($i = 0; $i < 100000; $i ++) {
    confirm_rust_fib_compiled(92);
}
?>

そしてこちらが結果です。

$ time php rust_fib.php

real    0m0.586s
user    0m0.342s
sys 0m0.221s

3倍以上も速くなっていることが分かりますね!Rustバージョンにとっての素敵なマイクロベンチマークでした!

結論

この記事から導くことができる結論といったものはほとんどありません。Rustを使ってPHPエクステンションを書くことが適切な場面が本当にあるのか、正直わかりません。でもまあ、RustとPHPとCに数時間熱中できる楽しい試みでした。

もし全てのコードを確認してご自身で試してみたければ、GitHubにまとめましたので、アクセスしてみてください。