2015年5月7日
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にまとめました ので、アクセスしてみてください。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa