道化的プログラミング

Google Cloud FunctionsでnativeビルドしたJavaを動かす

Google Cloud FunctionsはJavaをサポートしない。しかし、GraalVMでビルドしたshared libraryをNodeJS経由で呼び出すことならできるのではないかと試してみた。

やること

  • GraalVMで共有ライブラリをビルドする
  • 共有ライブラリ内の関数を呼び出す関数をNodeJSで実装する
  • JS関数に共有ライブラリを同梱しGoogle Cloud Functionsへデプロイ

GraalVMで共有ライブラリをビルドする

基本的にこちらと同様だが、GoogleCloudFunctionにデプロイするので、Linux向けの共有ライブラリをビルドする必要がある。
2019/04時点のGraalVM(Community Edition 1.0 RC15)のnative-imageは他のOS向けにビルドは出来ないようなので、MacなどではDockerを使うことになる。

具体的には、共有ライブラリを生成するコマンドを以下に置き換える。

docker run -it --rm -v $PWD:/work -w /work oracle/graalvm-ce:1.0.0-rc15 native-image --shared -H:Name=libnativeimpl -cp java

共有ライブラリ内の関数を呼び出す関数をNodeJSで実装する

お手軽にffi経由で呼び出すことにする。

準備

ffiをインストールする。ffiをインストールする過程で、node-gypがPython2.7を必要とするので、Python2.7もインストールしておく必要がある。

npm init -y
npm install ffi --save

jsコード

やっていることはこちらのCコードと同様だが、ヘッダファイル相当の情報を ffi.Library でのロード時に指定する。

型の対応関係は以下の通り。

  • C側で構造体へのポインタは ref.refType(ref.types.void)
  • 構造体へのポインタのポインタは ref.refType(ref.refType(ref.types.void))
  • 文字列は ref.types.CString

また、構造体のポインタを渡すような場合は、先立って ref.alloc で構造体のメモリを確保しておく必要がある。

その他の実装での注意点は以下の通り。

  • あとで同梱してデプロイするので、実行ファイルと共有ライブラリを同じディレクトリに格納し、実行ファイルのあるディレクトリからロードするよう実装する
  • GoogleCloudFunctionsのNodeJSのHTTPはexpress互換らしい
const ref = require('ref');
const ffi = require('ffi');

// ライブラリをロード
const libJava = ffi.Library(__dirname + '/libnativeimpl', {
  graal_create_isolate: [
    ref.types.int, [
      ref.refType(ref.types.void),
      ref.refType(ref.types.void),
      ref.refType(ref.refType(ref.types.void))
    ]],
  graal_tear_down_isolate: [
    ref.types.int, [
      ref.refType(ref.types.void)]
  ],
  Java_org_pkg_apinative_Native_hello: [
    ref.types.CString,
    [ref.refType(ref.types.void)]],
})

exports.handler = (req, res) => {
  const p_graal_isolatethread_t = ref.alloc(ref.refType(ref.types.void))

  const rc = libJava.graal_create_isolate(ref.NULL, ref.NULL, p_graal_isolatethread_t)
  if (rc !== 0) {
    res.send('error on isolate creation or attach')
    return;
  }

  const hello = libJava.Java_org_pkg_apinative_Native_hello(ref.deref(p_graal_isolatethread_t))
  res.send(hello);

  libJava.graal_tear_down_isolate(ref.deref(p_graal_isolatethread_t));
};

JS関数に共有ライブラリを同梱しGoogle Cloud Functionsへデプロイ

いよいよデプロイ。

当然GCPへの登録と、gcloudのインストールを済ませておく。

上記.jsファイルとと libnativeimpl.so を置いたディレクトリで以下のコマンドを実行。

gcloud functions deploy hello-graal-native-node --runtime nodejs8 --entry-point handler --trigger-http

1分ほど待ったのち、成功すれば以下のような表示がされるはず。

--trigger-httpDeploying function (may take a while - up to 2 minutes)...done.

availableMemoryMb: 256
entryPoint: handler
httpsTrigger:
  url: https://us-central1-xxxxxxxx.cloudfunctions.net/hello-graal-native-node
labels:
  deployment-tool: cli-gcloud
name: projects/xxxxxxxx/locations/us-central1/functions/hello-graal-native-node
runtime: nodejs8

...以下略

curl コマンドで叩いてみる。

$ curl https://us-central1-xxxxxxxx.cloudfunctions.net/hello-graal-native-node
hello from native lib

めでたい 🎉

なにがうれしい?

  • Javaを未サポートのGoogleCloudFunctionでもnative-imageで共有ライブラリ化すればワンチャン
  • むしろ素のJavaより起動が速い嬉しさもあるかも

コード一式

https://github.com/vertical-blank/google-cloud-function-graal-native-node