Androidアプリにjavascriptエンジン(DuktapeAndroid)を乗せる
目的
AndroidアプリにJavascriptエンジンを搭載する
背景
ユーザーがアプリ上の機能を自由に拡張できるような仕組みを作るため
解決策
Java上でJavaScriptを実行(eval)させる方法はいくつかあるが、下記のサイトを参考にDuktape Androidを使うことにした
comparison-shopping-searching-for-javascript-engines-for-android
duktape-android
基本的には上記の公式に従って導入すれば良い
依存関係の追加
- build.gradleの変更
implementation 'com.squareup.duktape:duktape-android:1.3.0'
とりあえずjavascriptを実行させる
自分はkotlinで書いているので下記になる
Duktape.create().use { x ->
Log.d("Greeting", x.evaluate("'hello world'.toUpperCase();").toString()) }
javaの人は公式のままで良いはず
Duktape duktape = Duktape.create();
try {
Log.d("Greeting", duktape.evaluate("'hello world'.toUpperCase();").toString());
} finally {
duktape.close();
}
実行してみて下記のようなログが出ればとりあえず導入は成功
2018-10-29 17:09:36.801 19934-19934/? D/Greeting: HELLO WORLD
javaからjavascriptへ値を渡す
やり方は色々あるが、まずは単純に整数をjsへ渡してみる
Duktape.create().use { x ->
x.set("test", Int::class.java, Integer.valueOf(100))
Log.d("Greeting", x.evaluate("'hello world'.toUpperCase() + test;").toString())
}
として実行するが...
java.lang.UnsupportedOperationException: Only interfaces can be bound. Received: class java.lang.Integer
と出るので、どうもインタフェース経由でしかやり取りできないっぽい
なので、下記のようにインタフェース経由で値を渡すようにする
- インタフェースを定義
interface Test {
fun test():Int
}
- オブジェクトを渡してjsで実行
ここではとりあえずtest.test()を実行すると100を返す関数を定義
Duktape.create().use { x ->
x.set("test", Test::class.java, object: Test{
override fun test(): Int {
return 100
}
})
Log.d("Greeting", x.evaluate("'hello world'.toUpperCase() + test.test();").toString())
}
- 結果確認
2018-10-29 17:44:22.055 21881-21881/* D/Greeting: HELLO WORLD100
javaからjavascriptの実行結果を取得する
こちらもインタフェースとして渡す
- js側で実装するためのインタフェースを定義
先程使ったインタフェースをそのまま使いまわす
interface Test {
fun test():Int
}
- js側でオブジェクトを作成してグルーバル変数へ入れる
Duktape.create().use { x ->
Log.d("Greeting", x.evaluate("var test = { test:function(){ return -1; } }; ").toString())
}
- java側で取得して実行
Log.d("Greeting", x.get("test", Test::class.java).test().toString())
が、エラーが出た
java.lang.IllegalArgumentException: In proxied method "test.test": Unsupported JavaScript return type int
javaからの引数としてサポートされているがjsからの戻り値としてintはサポートされていないようだ
なのでDoubleとして渡すことにする
インタフェースの戻り値をIntからDoubleへ
interface Test {
fun test():Double
}
実行すると成功した
2018-10-29 17:57:53.896 22867-22867/* D/Greeting: -1.0
課題
セキュリティには注意したほうが良い
今回はユーザーを信用することを前提としているのであまり気を使っていないが、もし一般配布する場合は実行範囲に制限を設けるなどをしないと任意のコードを注入されることになるため危険
また成果物であるスクリプトを共有できるような仕組みがある場合は更に危ない