Mengeksekusi JavaScript dan WebAssembly

Evaluasi JavaScript

Library Jetpack JavaScriptEngine menyediakan cara bagi aplikasi untuk mengevaluasi kode JavaScript tanpa membuat instance WebView.

Untuk aplikasi yang memerlukan evaluasi JavaScript non-interaktif, gunakan atribut Library JavaScriptEngine memiliki keuntungan berikut:

  • Konsumsi resource lebih rendah, karena tidak perlu mengalokasikan WebView di instance Compute Engine.

  • Dapat dilakukan di Service (tugas WorkManager).

  • Beberapa lingkungan terisolasi dengan overhead rendah, yang memungkinkan aplikasi untuk menjalankan beberapa cuplikan JavaScript secara bersamaan.

  • Kemampuan untuk meneruskan data dalam jumlah besar dengan menggunakan panggilan API.

Penggunaan Dasar

Untuk memulai, buat instance JavaScriptSandbox. Hal ini mewakili koneksi ke mesin JavaScript di luar proses.

ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
               JavaScriptSandbox.createConnectedInstanceAsync(context);

Sebaiknya sejajarkan siklus proses sandbox dengan yang memerlukan evaluasi JavaScript.

Misalnya, komponen yang menghosting sandbox mungkin berupa Activity atau Service. Satu Service dapat digunakan untuk mengenkapsulasi evaluasi JavaScript untuk semua komponen aplikasi.

Pertahankan instance JavaScriptSandbox karena alokasinya cukup mahal. Hanya satu instance JavaScriptSandbox per aplikasi yang diizinkan. Channel IllegalStateException ditampilkan saat aplikasi mencoba mengalokasikan instance JavaScriptSandbox kedua. Namun, jika beberapa lingkungan eksekusi diperlukan, beberapa instance JavaScriptIsolate dapat dialokasikan.

Jika tidak lagi digunakan, tutup instance sandbox untuk mengosongkan resource. Tujuan Instance JavaScriptSandbox mengimplementasikan antarmuka AutoCloseable, yang memungkinkan penggunaan try-with-resources untuk kasus penggunaan pemblokiran sederhana. Atau, pastikan siklus proses instance JavaScriptSandbox dikelola oleh komponen hosting, menutupnya dalam callback onStop() untuk Aktivitas atau selama onDestroy() untuk Layanan:

jsSandbox.close();

Instance JavaScriptIsolate mewakili konteks untuk dieksekusi kode JavaScript. Kunci keamanan dapat dialokasikan jika diperlukan, sehingga memberikan keamanan yang lemah batas untuk skrip dengan asal yang berbeda atau pengaktifan JavaScript serentak eksekusi karena JavaScript pada dasarnya berbentuk thread tunggal. Panggilan berikutnya ke instance yang sama memiliki status yang sama, sehingga beberapa data dapat dibuat terlebih dahulu, lalu proses nanti dalam instance JavaScriptIsolate yang sama.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

Rilis JavaScriptIsolate secara eksplisit dengan memanggil metode close(). Menutup instance isolasi yang menjalankan kode JavaScript (memiliki Future yang tidak lengkap) akan menghasilkan IsolateTerminatedException. Tujuan atau isolasi dibersihkan selanjutnya di latar belakang jika mendukung JS_FEATURE_ISOLATE_TERMINATION, sebagaimana dijelaskan dalam menangani error sandbox nanti di bagian ini kami. Jika tidak, pembersihan akan ditunda hingga semua evaluasi yang tertunda selesai atau {i>sandbox<i} ditutup.

Aplikasi dapat membuat dan mengakses instance JavaScriptIsolate dari thread apa pun.

Sekarang, aplikasi siap mengeksekusi beberapa kode JavaScript:

final String code = "function sum(a, b) { let r = a + b; return r.toString(); }; sum(3, 4)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);

Cuplikan JavaScript yang sama diformat dengan baik:

function sum(a, b) {
    let r = a + b;
    return r.toString(); // make sure we return String instance
};

// Calculate and evaluate the expression
// NOTE: We are not in a function scope and the `return` keyword
// should not be used. The result of the evaluation is the value
// the last expression evaluates to.
sum(3, 4);

Cuplikan kode diteruskan sebagai String dan hasilnya dikirim sebagai String. Perhatikan bahwa memanggil evaluateJavaScriptAsync() akan menampilkan hasil evaluasi hasil ekspresi terakhir dalam kode JavaScript. Ini harus dari jenis String JavaScript; jika tidak, API library akan menampilkan nilai kosong. Kode JavaScript tidak boleh menggunakan kata kunci return. Jika sandbox mendukung fitur tertentu, jenis nilai yang ditampilkan tambahan (misalnya, Promise yang di-resolve menjadi String) mungkin dimungkinkan.

Library ini juga mendukung evaluasi skrip dalam bentuk AssetFileDescriptor atau ParcelFileDescriptor. Lihat evaluateJavaScriptAsync(AssetFileDescriptor) dan evaluateJavaScriptAsync(ParcelFileDescriptor) untuk detail selengkapnya. API ini lebih cocok untuk mengevaluasi dari file pada disk atau dalam aplikasi direktori.

Library ini juga mendukung logging konsol yang dapat digunakan untuk proses debug tujuan. Hal ini dapat disiapkan menggunakan setConsoleCallback().

Karena konteksnya tetap ada, Anda dapat mengupload kode dan mengeksekusinya beberapa kali selama masa aktif JavaScriptIsolate:

String jsFunction = "function sum(a, b) { let r = a + b; return r.toString(); }";
ListenableFuture<String> func = js.evaluateJavaScriptAsync(jsFunction);
String twoPlusThreeCode = "let five = sum(2, 3); five";
ListenableFuture<String> r1 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(twoPlusThreeCode)
       , executor);
String twoPlusThree = r1.get(5, TimeUnit.SECONDS);

String fourPlusFiveCode = "sum(4, parseInt(five))";
ListenableFuture<String> r2 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(fourPlusFiveCode)
       , executor);
String fourPlusFive = r2.get(5, TimeUnit.SECONDS);

Tentu saja, variabel juga persisten, sehingga Anda dapat melanjutkan langkah sebelumnya cuplikan dengan:

String defineResult = "let result = sum(11, 22);";
ListenableFuture<String> r3 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(defineResult)
       , executor);
String unused = r3.get(5, TimeUnit.SECONDS);

String obtainValue = "result";
ListenableFuture<String> r4 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(obtainValue)
       , executor);
String value = r4.get(5, TimeUnit.SECONDS);

Misalnya, cuplikan lengkap untuk mengalokasikan semua objek dan mengeksekusi kode JavaScript mungkin akan terlihat seperti berikut:

final ListenableFuture<JavaScriptSandbox> sandbox
       = JavaScriptSandbox.createConnectedInstanceAsync(this);
final ListenableFuture<JavaScriptIsolate> isolate
       = Futures.transform(sandbox,
               input -> (jsSandBox = input).createIsolate(),
               executor);
final ListenableFuture<String> js
       = Futures.transformAsync(isolate,
               isolate -> (jsIsolate = isolate).evaluateJavaScriptAsync("'PASS OK'"),
               executor);
Futures.addCallback(js,
       new FutureCallback<String>() {
           @Override
           public void onSuccess(String result) {
               text.append(result);
           }
           @Override
           public void onFailure(Throwable t) {
               text.append(t.getMessage());
           }
       },
       mainThreadExecutor);

Sebaiknya Anda menggunakan fitur coba dengan sumber daya untuk memastikan resource dilepaskan dan tidak digunakan lagi. Menutup hasil sandbox dalam semua evaluasi yang tertunda di semua instance JavaScriptIsolate gagal dengan SandboxDeadException. Saat evaluasi JavaScript menemukan error, berarti JavaScriptException akan dibuat. Mengacu pada subclass-nya untuk pengecualian yang lebih spesifik.

Menangani Error Sandbox

Semua JavaScript dieksekusi dalam proses sandbox terpisah dari proses utama aplikasi Anda. Jika kode JavaScript menyebabkan proses dalam sandbox ini hingga tidak bekerja, misalnya, karena kehabisan batas memori, tidak akan terpengaruh.

Error sandbox akan menyebabkan semua isolasi dalam sandbox tersebut dihentikan. Paling sering gejala yang jelas dari hal ini adalah bahwa semua evaluasi akan mulai gagal dengan IsolateTerminatedException Tergantung pada situasinya, pengecualian khusus seperti SandboxDeadException atau MemoryLimitExceededException dapat ditampilkan.

Menangani error untuk masing-masing evaluasi tidak selalu praktis. Selain itu, isolasi dapat dihentikan di luar permintaan eksplisit evaluasi karena tugas latar belakang atau evaluasi di isolasi lain. Tabrakan logika penanganan dapat dipusatkan dengan melampirkan callback menggunakan JavaScriptIsolate.addOnTerminatedCallback()

final ListenableFuture<JavaScriptSandbox> sandboxFuture =
    JavaScriptSandbox.createConnectedInstanceAsync(this);
final ListenableFuture<JavaScriptIsolate> isolateFuture =
    Futures.transform(sandboxFuture, sandbox -> {
      final IsolateStartupParameters startupParams = new IsolateStartupParameters();
      if (sandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE)) {
        startupParams.setMaxHeapSizeBytes(100_000_000);
      }
      return sandbox.createIsolate(startupParams);
    }, executor);
Futures.transform(isolateFuture,
    isolate -> {
      // Add a crash handler
      isolate.addOnTerminatedCallback(executor, terminationInfo -> {
        Log.e(TAG, "The isolate crashed: " + terminationInfo);
      });
      // Cause a crash (eventually)
      isolate.evaluateJavaScriptAsync("Array(1_000_000_000).fill(1)");
      return null;
    }, executor);

Fitur Sandbox Opsional

Bergantung pada versi WebView yang mendasarinya, implementasi sandbox mungkin memiliki set fitur yang berbeda. Jadi, Anda perlu melakukan kueri masing-masing fitur menggunakan JavaScriptSandbox.isFeatureSupported(...). Penting untuk memeriksa status fitur sebelum memanggil metode yang mengandalkan fitur ini.

Metode JavaScriptIsolate yang mungkin tidak tersedia di semua tempat dianotasi dengan RequiresFeature, sehingga lebih mudah untuk menemukan memanggil dalam kode.

Parameter Penerusan

Jika JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT adalah didukung, permintaan evaluasi yang dikirim ke mesin JavaScript tidak terikat oleh batas transaksi binder. Jika fitur ini tidak didukung, semua data yang akan JavaScriptEngine terjadi melalui transaksi Binder. Persyaratan batas ukuran transaksi berlaku untuk setiap panggilan yang meneruskan data atau menampilkan data.

Respons akan selalu ditampilkan sebagai String dan tunduk pada Binder batas ukuran maksimum transaksi jika JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT bukan didukung. Nilai non-string harus dikonversi secara eksplisit ke String JavaScript jika tidak, string kosong akan ditampilkan. Jika JS_FEATURE_PROMISE_RETURN didukung, kode JavaScript mungkin juga akan menampilkan Promise me-resolve ke String.

Untuk meneruskan array byte besar ke instance JavaScriptIsolate, Anda dapat menggunakan provideNamedData(...) API. Penggunaan API ini tidak terikat oleh batas transaksi Binder. Setiap array byte harus diteruskan menggunakan yang tidak dapat digunakan kembali.

if (sandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER)) {
    js.provideNamedData("data-1", "Hello Android!".getBytes(StandardCharsets.US_ASCII));
    final String jsCode = "android.consumeNamedDataAsArrayBuffer('data-1').then((value) => { return String.fromCharCode.apply(null, new Uint8Array(value)); });";
    ListenableFuture<String> msg = js.evaluateJavaScriptAsync(jsCode);
    String response = msg.get(5, TimeUnit.SECONDS);
}

Menjalankan Kode Wasm

Kode WebAssembly (Wasm) dapat diteruskan menggunakan provideNamedData(...) API, kemudian dikompilasi dan dieksekusi dengan cara biasa, seperti yang ditunjukkan di bawah ini.

final byte[] hello_world_wasm = {
   0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x0a ,0x02 ,0x60 ,0x02 ,0x7f ,0x7f ,0x01,
   0x7f ,0x60 ,0x00 ,0x00 ,0x03 ,0x03 ,0x02 ,0x00 ,0x01 ,0x04 ,0x04 ,0x01 ,0x70 ,0x00 ,0x01 ,0x05,
   0x03 ,0x01 ,0x00 ,0x00 ,0x06 ,0x06 ,0x01 ,0x7f ,0x00 ,0x41 ,0x08 ,0x0b ,0x07 ,0x18 ,0x03 ,0x06,
   0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,0x79 ,0x02 ,0x00 ,0x05 ,0x74 ,0x61 ,0x62 ,0x6c ,0x65 ,0x01 ,0x00,
   0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x00 ,0x09 ,0x07 ,0x01 ,0x00 ,0x41 ,0x00 ,0x0b ,0x01 ,0x01 ,0x0a,
   0x0c ,0x02 ,0x07 ,0x00 ,0x20 ,0x00 ,0x20 ,0x01 ,0x6a ,0x0b ,0x02 ,0x00 ,0x0b,
};
final String jsCode = "(async ()=>{" +
       "const wasm = await android.consumeNamedDataAsArrayBuffer('wasm-1');" +
       "const module = await WebAssembly.compile(wasm);" +
       "const instance = WebAssembly.instance(module);" +
       "return instance.exports.add(20, 22).toString();" +
       "})()";
// Ensure that the name has not been used before.
js.provideNamedData("wasm-1", hello_world_wasm);
FluentFuture.from(js.evaluateJavaScriptAsync(jsCode))
           .transform(this::println, mainThreadExecutor)
           .catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
}

Pemisahan JavaScriptIsolate

Semua instance JavaScriptIsolate tidak saling bergantung dan tidak bagikan apa saja. Cuplikan berikut menghasilkan

Hi from AAA!5

dan

Uncaught Reference Error: a is not defined

karena instance ”jsTwo” tidak memiliki visibilitas objek yang dibuat di “jsOne”.

JavaScriptIsolate jsOne = engine.obtainJavaScriptIsolate();
String jsCodeOne = "let x = 5; function a() { return 'Hi from AAA!'; } a() + x";
JavaScriptIsolate jsTwo = engine.obtainJavaScriptIsolate();
String jsCodeTwo = "a() + x";
FluentFuture.from(jsOne.evaluateJavaScriptAsync(jsCodeOne))
       .transform(this::println, mainThreadExecutor)
       .catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);

FluentFuture.from(jsTwo.evaluateJavaScriptAsync(jsCodeTwo))
       .transform(this::println, mainThreadExecutor)
       .catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);

Dukungan Kotlin

Untuk menggunakan library Jetpack ini dengan coroutine Kotlin, tambahkan dependensi ke kotlinx-coroutines-guava Hal ini memungkinkan integrasi dengan ListenableFuture.

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}

API library Jetpack kini dapat dipanggil dari cakupan coroutine, seperti ditunjukkan di bawah ini:

// Launch a coroutine
lifecycleScope.launch {
    val jsSandbox = JavaScriptSandbox
            .createConnectedInstanceAsync(applicationContext)
            .await()
    val jsIsolate = jsSandbox.createIsolate()
    val resultFuture = jsIsolate.evaluateJavaScriptAsync("PASS")

    // Await the result
    textBox.text = resultFuture.await()
    // Or add a callback
    Futures.addCallback<String>(
        resultFuture, object : FutureCallback<String?> {
            override fun onSuccess(result: String?) {
                textBox.text = result
            }
            override fun onFailure(t: Throwable) {
                // Handle errors
            }
        },
        mainExecutor
    )
}

Parameter Konfigurasi

Saat meminta instance lingkungan yang terisolasi, Anda dapat menyesuaikan instance konfigurasi Anda. Untuk mengubah konfigurasi, teruskan IsolateStartupParameters untuk JavaScriptSandbox.createIsolate(...)

Parameter saat ini memungkinkan penetapan ukuran heap maksimum dan ukuran maksimum untuk evaluasi error dan nilai return.