サーバーサイドエンジニアの田実です!
ヤプリではPHPとGoを使っています。ということで(?)、PHPからGoを呼び出す方法について紹介します。 なおこの記事はGoからPHPを呼び出す方法の姉妹投稿(?)となっております。
※この記事は PHP Advent Calendar 2021の18日目の記事です!
IPC
PHPからGoを呼び出す手段としてspiral/goridgeというパッケージがあります。
GoのIPC用のサーバーを予め立てておきPHPでIPCでGoにアクセスする仕組みです。 本質的にはHTTPリクエストと変わらないのですが、IPCなのでフットプリントが少なく高速です。 Go製のPHPサーバーであるRoadRunnerはこちらのライブラリを使って動いています。
READMEの通り、Goのサーバーをこんな感じで定義します。
package main import ( "fmt" "net" "net/rpc" goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" ) type App struct{} func (s *App) Hi(name string, r *string) error { *r = fmt.Sprintf("Hello, %s!", name) return nil } func main() { ln, err := net.Listen("unix", "/tmp/rpc.sock") if err != nil { panic(err) } _ = rpc.Register(new(App)) for { conn, err := ln.Accept() if err != nil { continue } _ = conn go rpc.ServeCodec(goridgeRpc.NewCodec(conn)) } }
PHP側は composer require spiral/goridge
でパッケージをインストールして以下のようにして呼び出します。
<?php use Spiral\Goridge; require "vendor/autoload.php"; $rpc = new Goridge\RPC\RPC(Goridge\Relay::create('unix:///tmp/rpc.sock')); echo $rpc->call("App.Hi", "Antony");
手軽な方法なのですが、Goのサーバーを管理する必要があったり、直接呼び出しているわけではないので微妙な感じです…。
Cの共有ライブラリをFFIで読み込む
Goを使ってCの共有ライブラリを作成し、それをPHPからFFI経由で読み込むことで何となくPHPからGoを呼び出している気持ちになります。
package main import ( "C" "fmt" ) //export hello func hello() { fmt.Println("world") } //export concat func concat(a, b *C.char) *C.char { return C.CString(C.GoString(a) + C.GoString(b)) } func main() {}
ビルドは-buildmode c-shared
を指定すればOK
$ go build -o example.so -buildmode c-shared main.go
PHP側はヘッダを定義しつつC共有ライブラリを読み込むことで呼び出せます。
<?php $ffi = FFI::cdef(<<<EOS const char* concat(char* a, char* b); void hello(); EOS, __DIR__ .'/example.so'); $ffi->hello(); $result = $ffi->concat('hoge', 'fuga'); echo $result;
ヘッダの定義やCStringとGoStringの変換などcgo系のハンドリングが若干面倒です…。
Cの共有ライブラリをarnaud-lb/php-goで読み込む
FFI経由ではなく arnaud-lb/php-go
というパッケージ経由で読み込むとcgoのハンドリングの手間が省けます。
github.com
共有ライブラリを読みこむPHPのC拡張をビルドします。
$ git clone git@github.com:arnaud-lb/php-go.git
$ cd ./php-go/ext
$ phpize && ./configure && make && sudo make install
Goのコードを実装します。
package main import ( "strings" "github.com/arnaud-lb/php-go/php-go" ) var _ = php.Export("example", map[string]interface{}{ "toUpper": strings.ToUpper, }) func main() {}
共有ライブラリとしてビルドします。
$ go build -o example.so -buildmode c-shared .
こんな感じでPHPスクリプトを書きます。
<?php $module = phpgo_load("/path/to/example.so", "example"); ReflectionClass::export($module); var_dump($module->toUpper("foo"));
phpgoの共有ライブラリをextensionとして有効化してPHPを実行します。
$ php -dextension=phpgo.so test.php # => FOO
cgoのハンドリングをする手間がなくなりましたが、手順が若干複雑です…。
Goのインタープリターを実装する
PHPでGoをパースしてASTを構築し、それを自前のインタープリターで解釈します。 残念ながらPHPで書かれたGoのパーサーが見つからなかったので、パーサーも自前で実装する必要があります。
ということで実装したパーサー・インタープリターはこちらです。 github.com
実行方法はこんな感じ
<?php require_once 'vendor/autoload.php'; $runner = new \GoInterpreter\Runner(); $runner->run('/path/to/gofile', 'func_name');
簡単!
パーサーはANTLRで実装しており、GoのgrammarファイルとPHPランタイムを利用しています。
しかしながら、実行してみるとわかるのですが、めちゃくちゃ遅いです。簡単な四則演算をreturnさせるだけでも10秒くらいかかります…w
ANTLRでのパースに時間がかかってそうなのですが、ネタ枠なのでこれ以上追求するのは諦めました。
おまけ: ANTLRを使ったGoのパーサーの作り方
ANTLRのインストール
$ curl -O https://www.antlr.org/download/antlr-4.9-complete.jar $ mv antlr-4.9-complete.jar /path/to/lib
環境変数やaliasを設定
export CLASSPATH=".:/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH" alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH" org.antlr.v4.Tool' alias grun='java -Xmx500M -cp "/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig'
grammarファイルからパーサーの自動生成
$ composer require antlr/antlr4-php-runtime $ antlr4 -visitor -Dlanguage=PHP GoParser.g4
自動生成されたパーサーで生成されたASTはインタープリターなどを実装するには構造的に扱いづらいのでアプリケーションで利用しやすい構造に置き換えます。
<?php class ASTConverter { // ... public function visitPrimaryExpr(Context\PrimaryExprContext $context) { if ($context->primaryExpr()) { $expr = $context->primaryExpr()->accept($this); if ($context->IDENTIFIER()) { $expr->child = new Identifier($context->IDENTIFIER()->getText(), null); return $expr; } if ($context->arguments()) { return new MethodCall($expr, new Arguments($context->arguments()->accept($this))); } return null; } } // ...
ASTを作ったらそれに対してvisitorパターンでNodeを順に舐めていき処理を実行します。
<?php class Interpreter { public function visit(Node $node) { if ($node instanceof File) { foreach ($node->functions['main']->statements as $statement) { $statement->accept($this); } } if ($node instanceof MethodCall) { $arguments = []; foreach ($node->arguments as $argument) { $arguments[] = $argument->accept($this); } $receiver = $node->expr->accept($this); if ($receiver === 'fmt.Println') { echo $arguments[0]->value . PHP_EOL; } } // ...
あとはこんな感じで実行すればOK。
<?php $input = InputStream::fromPath('/path/to/gofile'); $lexer = new GoLexer($input); $tokens = new CommonTokenStream($lexer); $parser = new GoParser($tokens); $parser->addErrorListener(new DiagnosticErrorListener()); $parser->setBuildParseTree(true); $file = $parser->sourceFile(); $converter = new ASTConverter(); $tree = $converter->visit($file); $interpreter = new Interpreter(); $interpreter->visit($tree);
まとめ
PHPからGoを呼び出す方法を紹介しました!
ネタでインタープリター作ろうと思ったら意外と共有ライブラリ系のアプローチがイケてるなと思いましたw