Como executar JavaScript e WebAssembly

Avaliação de JavaScript

A biblioteca JavaScriptEngine do Jetpack oferece uma maneira de um aplicativo avaliar o código JavaScript sem criar uma instância de WebView.

Para aplicativos que exigem avaliação JavaScript não interativa, usar o método A biblioteca JavaScriptEngine tem as seguintes vantagens:

  • Menor consumo de recursos, já que não é necessário alocar um WebView instância.

  • Pode ser feita em um serviço (tarefa WorkManager).

  • Vários ambientes isolados com baixa overhead, permitindo que o aplicativo executar vários snippets de JavaScript simultaneamente.

  • Capacidade de transmitir grandes quantidades de dados usando uma chamada de API.

Uso básico

Para começar, crie uma instância de JavaScriptSandbox. Isso representa ao mecanismo JavaScript fora do processo.

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

É recomendado alinhar o ciclo de vida do sandbox com o ciclo de vida do que precisa de avaliação do JavaScript.

Por exemplo, um componente que hospeda o sandbox pode ser um Activity ou um Service. Um único Service pode ser usado para encapsular a avaliação do JavaScript para todos os componentes do aplicativo.

Manter a instância JavaScriptSandbox porque sua alocação é bastante caro. Só é permitida uma instância do JavaScriptSandbox por aplicativo. Um IllegalStateException é gerado quando um aplicativo tenta alocar um segunda instância de JavaScriptSandbox. No entanto, se vários ambientes de execução são necessárias, várias instâncias JavaScriptIsolate podem ser alocadas.

Quando ela não for mais usada, feche a instância do sandbox para liberar recursos. A A instância JavaScriptSandbox implementa uma interface AutoCloseable, que permite o uso de tentativa com recursos para casos de uso simples com bloqueio. Como alternativa, verifique se o ciclo de vida da instância do JavaScriptSandbox é gerenciado por componente de hospedagem, fechando-o no callback onStop() para uma atividade ou durante onDestroy() para um Serviço:

jsSandbox.close();

Uma instância JavaScriptIsolate representa um contexto para executar Código JavaScript. Eles podem ser alocados quando necessário, oferecendo pouca segurança limites para scripts de diferentes origens ou ativação de JavaScript simultâneo já que o JavaScript tem um único thread por natureza. Chamadas subsequentes para da mesma instância compartilham o mesmo estado. Por isso, é possível criar alguns dados e processar depois na mesma instância de JavaScriptIsolate.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

Libere JavaScriptIsolate explicitamente chamando o método close() dele. Como fechar uma instância isolada executando código JavaScript (ter um Future incompleto) resulta em uma IsolateTerminatedException. A O isolamento é limpo posteriormente em segundo plano se a implementação é compatível com JS_FEATURE_ISOLATE_TERMINATION, conforme descrito nas seção Como lidar com falhas no sandbox mais adiante página. Caso contrário, a limpeza será adiada até que todas as avaliações pendentes sejam concluído ou a sandbox está fechada.

Um aplicativo pode criar e acessar uma instância do JavaScriptIsolate pela qualquer thread.

Agora, o aplicativo está pronto para executar códigos 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);

O mesmo snippet de JavaScript formatado corretamente:

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);

O snippet de código é transmitido como um String e o resultado, como um String. Chamar evaluateJavaScriptAsync() retorna os valores da última expressão no código JavaScript. Deve ser do tipo String JavaScript; Caso contrário, a API da biblioteca retornará um valor vazio. O código JavaScript não deve usar uma palavra-chave return. Se a sandbox oferece suporte a determinados recursos e outros tipos de retorno (por exemplo, um Promise que se resolve em um String).

A biblioteca também oferece suporte à avaliação de scripts na forma de um AssetFileDescriptor ou um ParcelFileDescriptor. Consulte evaluateJavaScriptAsync(AssetFileDescriptor) e evaluateJavaScriptAsync(ParcelFileDescriptor) para mais detalhes. Essas APIs são mais adequadas para avaliar usando um arquivo em disco ou no app diretórios.

A biblioteca também oferece suporte ao registro do console, que pode ser usado para depuração propósitos. Isso pode ser configurado usando setConsoleCallback().

Como o contexto persiste, você pode fazer upload do código e executá-lo várias vezes durante a vida útil do 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);

Claro, as variáveis também são persistentes, então você pode continuar com a por:

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);

Por exemplo, o snippet completo para alocar todos os objetos necessários e A execução de um código JavaScript pode ser semelhante ao seguinte:

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);

Recomendamos usar "testar com recursos" para garantir que todos os recursos são liberados e não são mais usados. Como fechar os resultados do sandbox em todas as avaliações pendentes em todas as instâncias de JavaScriptIsolate com falha com um SandboxDeadException. Quando a avaliação JavaScript encontra um erro, uma JavaScriptException é criada. Consulte as subclasses para exceções mais específicas.

Tratamento de falhas do sandbox

Todo JavaScript é executado em um processo em sandbox separado, fora da sua o processo principal do aplicativo. Se o código JavaScript gerar esse processo no sandbox para falhar, por exemplo, esgotando um limite de memória, a principal e o processo não serão afetados.

Uma falha no sandbox fará com que todos os isolamentos dessa sandbox sejam encerrados. A maior um sintoma óbvio disso é que todas as avaliações vão falhar IsolateTerminatedException Dependendo das circunstâncias, exceções específicas, como SandboxDeadException ou MemoryLimitExceededException pode ser gerada.

Nem sempre é prático lidar com falhas para cada avaliação individual. Além disso, um isolamento pode ser encerrado fora de uma zona devido a tarefas em segundo plano ou avaliações em outros isolamentos. A falha de processamento de dados pode ser centralizada anexando um callback usando 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);

Recursos opcionais do sandbox

Dependendo da versão do WebView, uma implementação de sandbox pode ter diferentes conjuntos de recursos disponíveis. Portanto, é necessário consultar cada recurso usando JavaScriptSandbox.isFeatureSupported(...). É importante para verificar o status do recurso antes de chamar métodos que dependem deles.

JavaScriptIsolate que podem não estar disponíveis em todos os lugares são com a anotação RequiresFeature, facilitando a detecção desses no código.

Parâmetros de transmissão

Se JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT for suportado, as solicitações de avaliação enviadas ao mecanismo JavaScript não são vinculadas pelos limites de transação do binder. Se o recurso não for suportado, todos os dados a JavaScriptEngine ocorre por meio de uma transação de vinculação. A equipe limite de tamanho da transação se aplica a todas as chamadas que transmitem dados ou retorna dados.

A resposta é sempre retornada como uma string e está sujeita à o limite máximo de tamanho da transação se JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT não é suporte. Os valores sem string precisam ser explicitamente convertidos em uma string JavaScript. caso contrário, uma string vazia será retornada. Se JS_FEATURE_PROMISE_RETURN for compatível, o código JavaScript poderá retornar uma promessa resolver para um String.

Para transmitir matrizes de bytes grandes à instância JavaScriptIsolate, podem usar a API provideNamedData(...). O uso dessa API não está sujeito aos os limites de transação do binder. Cada matriz de bytes precisa ser transmitida usando uma string identificador que não pode ser reutilizado.

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);
}

Como executar o código Wasm

O código WebAssembly (Wasm) pode ser transmitido usando o método provideNamedData(...) API do BigQuery, então compilada e executada da maneira usual, como demonstrado abaixo.

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);
}

Separação JavaScript-Isolar

Todas as instâncias de JavaScriptIsolate são independentes umas das outras e não compartilhar qualquer coisa. O snippet a seguir resulta em

Hi from AAA!5

e

Uncaught Reference Error: a is not defined

porque a instância "jsTwo" não tem visibilidade dos objetos criados em "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);

Suporte ao Kotlin

Para usar essa biblioteca do Jetpack com corrotinas do Kotlin, adicione uma dependência ao kotlinx-coroutines-guava Isso permite a integração com ListenableFuture:

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

As APIs da biblioteca do Jetpack agora podem ser chamadas em um escopo de corrotina, como demonstrados abaixo:

// 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
    )
}

Parâmetros de configuração

Ao solicitar uma instância de ambiente isolada, você pode ajustar seu configuração do Terraform. Para ajustar a configuração, passe o valor-chave IsolateStartupParameters para JavaScriptSandbox.createIsolate(...).

No momento, os parâmetros permitem especificar os tamanhos máximos de heap para valores de retorno de avaliação e erros.