JavaScript মূল্যায়ন
Jetpack লাইব্রেরি JavaScriptEngine কোনও অ্যাপ্লিকেশনকে WebView ইনস্ট্যান্স তৈরি না করেই জাভাস্ক্রিপ্ট কোড মূল্যায়ন করার একটি উপায় প্রদান করে।
যেসব অ্যাপ্লিকেশনের জন্য নন-ইন্টার্যাক্টিভ জাভাস্ক্রিপ্ট মূল্যায়ন প্রয়োজন, সেগুলির ক্ষেত্রে JavaScriptEngine লাইব্রেরি ব্যবহার করলে নিম্নলিখিত সুবিধা পাওয়া যায়:
WebView ইনস্ট্যান্স বরাদ্দ করার প্রয়োজন নেই বলে রিসোর্স কম খরচ হয়।
কোনও পরিষেবাতে (WorkManager টাস্ক) করা যেতে পারে।
কম ওভারহেড সহ একাধিক আইসোলেটেড এনভায়রনমেন্ট, যা অ্যাপ্লিকেশনকে একই সাথে একাধিক JavaScript স্নিপেট রান করতে দেয়।
API কল ব্যবহার করে প্রচুর পরিমাণে ডেটা পাস করার ক্ষমতা।
প্রাথমিক ব্যবহার
শুরু করতে, JavaScriptSandbox-এর একটি ইনস্ট্যান্স তৈরি করুন। এটি আউট-অফ-প্রসেস জাভাস্ক্রিপ্ট ইঞ্জিনের সাথে
কানেকশনকে দেখায়।
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
স্যান্ডবক্সের লাইফসাইকেলকে সেই কম্পোনেন্টের লাইফসাইকেলের সাথে অ্যালাইন করার জন্য সাজেস্ট করা হয় যেটির জাভাস্ক্রিপ্ট মূল্যায়ন প্রয়োজন।
যেমন, স্যান্ডবক্স হোস্ট করা কম্পোনেন্ট Activity বা
Service হতে পারে। সব অ্যাপ্লিকেশন কম্পোনেন্টের জন্য জাভাস্ক্রিপ্ট মূল্যায়ন এনক্যাপসুলেট করতে
একটিমাত্র Service ব্যবহার করা হতে পারে।
JavaScriptSandbox ইনস্ট্যান্স বজায় রাখুন কারণ এটির অ্যালোকেশন বেশ
ব্যয়বহুল। প্রতিটি অ্যাপ্লিকেশনের জন্য শুধুমাত্র একটি JavaScriptSandbox ইনস্ট্যান্স অনুমোদিত। কোনও অ্যাপ্লিকেশন দ্বিতীয় JavaScriptSandbox ইনস্ট্যান্স
অ্যালোকেট করার চেষ্টা করলে
IllegalStateException থ্রো করা হয়। তবে, একাধিক এক্সিকিউশন এনভায়রনমেন্টের
প্রয়োজন হলে, একাধিক JavaScriptIsolate ইনস্ট্যান্স বরাদ্দ করা যেতে পারে।
এটি আর ব্যবহার করা না হলে, রিসোর্স খালি করতে স্যান্ডবক্স ইনস্ট্যান্স বন্ধ করুন। JavaScriptSandbox ইনস্ট্যান্স একটি AutoCloseable ইন্টারফেস প্রয়োগ করে, যা
সহজ ব্লকিং ব্যবহারের ক্ষেত্রে রিসোর্স সহ চেষ্টা করার সুবিধা দেয়।
বিকল্প হিসেবে, নিশ্চিত করুন যে JavaScriptSandbox ইনস্ট্যান্স লাইফসাইকেল হোস্টিং কম্পোনেন্ট দ্বারা ম্যানেজ করা হচ্ছে, এটি অ্যাক্টিভিটির জন্য onStop() কলব্যাকে বা
সার্ভিসের জন্য onDestroy() চলাকালীন বন্ধ করা হচ্ছে:
jsSandbox.close();
JavaScriptIsolate ইনস্ট্যান্স জাভাস্ক্রিপ্ট কোড এক্সিকিউট করার জন্য
কনটেক্সটকে দেখায়। প্রয়োজন হলে এগুলি বরাদ্দ করা যেতে পারে, যা বিভিন্ন সোর্স থেকে আসা স্ক্রিপ্টের জন্য দুর্বল নিরাপত্তা
সীমানা প্রদান করে অথবা একই সাথে জাভাস্ক্রিপ্ট
চালানোর সুবিধা দেয়, কারণ জাভাস্ক্রিপ্ট স্বভাবতই সিঙ্গল-থ্রেডেড। একই ইনস্ট্যান্সে
পরবর্তী কল একই স্টেট শেয়ার করে, তাই প্রথমে কিছু ডেটা তৈরি করা
এবং পরে JavaScriptIsolate-এর একই ইনস্ট্যান্সে তা প্রসেস করা সম্ভব।
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
JavaScriptIsolate-এর close() পদ্ধতি কল করে স্পষ্টভাবে রিলিজ করুন।
জাভাস্ক্রিপ্ট কোড রান করছে এমন একটি আইসোলেট ইনস্ট্যান্স বন্ধ করলে
(যার Future অসম্পূর্ণ আছে) তার ফলে IsolateTerminatedException হয়। ইমপ্লিমেন্টেশন
JS_FEATURE_ISOLATE_TERMINATION সাপোর্ট করলে,
আইসোলেটটি পরে ব্যাকগ্রাউন্ডে ক্লিন-আপ করা হয়, যেমনটি এই পৃষ্ঠার পরে
স্যান্ডবক্স ক্র্যাশ ম্যানেজ করা বিভাগে
বর্ণনা করা হয়েছে। অন্যথায়, সবকটি বাকি থাকা মূল্যায়ন সম্পূর্ণ না হওয়া পর্যন্ত অথবা স্যান্ডবক্স বন্ধ না হওয়া পর্যন্ত
ক্লিন-আপ স্থগিত রাখা হয়।
কোনও অ্যাপ্লিকেশন যেকোনও থ্রেড থেকে JavaScriptIsolate ইনস্ট্যান্স তৈরি ও অ্যাক্সেস করতে
পারে।
এখন, অ্যাপ্লিকেশনটি কিছু জাভাস্ক্রিপ্ট কোড এক্সিকিউট করার জন্য প্রস্তুত:
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);
একই জাভাস্ক্রিপ্ট স্নিপেট সুন্দরভাবে ফর্ম্যাট করা হয়েছে:
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);
কোড স্নিপেট String হিসেবে পাস করা হয় এবং ফলাফল String হিসেবে ডেলিভার করা হয়।
মনে রাখবেন, evaluateJavaScriptAsync() কল করলে জাভাস্ক্রিপ্ট কোডে
শেষ এক্সপ্রেশনের মূল্যায়ন করা ফলাফল পাওয়া যায়। এটি অবশ্যই
জাভাস্ক্রিপ্ট String ধরনের হতে হবে; অন্যথায়, লাইব্রেরি API একটি খালি মান প্রদান করে।
জাভাস্ক্রিপ্ট কোডে return কীওয়ার্ড ব্যবহার করা উচিত নয়। স্যান্ডবক্স
নির্দিষ্ট কিছু ফিচার সাপোর্ট করলে, অতিরিক্ত রিটার্ন টাইপ (যেমন, একটি Promise
যা একটি String-এ সমাধান করে) সম্ভব হতে পারে।
এছাড়াও, লাইব্রেরি AssetFileDescriptor বা ParcelFileDescriptor-এর মতো স্ক্রিপ্টের মূল্যায়ন করতে সাহায্য করে। আরও বিবরণের জন্য
evaluateJavaScriptAsync(AssetFileDescriptor) এবং
evaluateJavaScriptAsync(ParcelFileDescriptor) দেখুন।
ডিস্কের ফাইল বা অ্যাপ ডিরেক্টরি থেকে মূল্যায়ন করার জন্য এইসব API আরও উপযুক্ত।
এছাড়াও, লাইব্রেরি কনসোল লগিং সাপোর্ট করে যা ডিবাগিংয়ের উদ্দেশ্যে ব্যবহার করা
যেতে পারে। setConsoleCallback() ব্যবহার করে এটি সেট-আপ করা যাবে।
যেহেতু প্রসঙ্গটি থেকে যায়, তাই আপনি 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);
অবশ্যই, ভেরিয়েবলও স্থায়ী, তাই আপনি আগের স্নিপেট চালিয়ে যেতে পারবেন:
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);
যেমন, সমস্ত প্রয়োজনীয় অবজেক্ট বরাদ্দ করা এবং জাভাস্ক্রিপ্ট কোড কার্যকর করার জন্য সম্পূর্ণ স্নিপেটটি নিম্নলিখিতটির মতো দেখতে হতে পারে:
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);
সব বরাদ্দ করা রিসোর্স রিলিজ করা হয়েছে এবং আর ব্যবহার করা হচ্ছে না তা নিশ্চিত করতে,
আপনাকে try-with-resources ব্যবহার করার জন্য সাজেস্ট করা হয়। স্যান্ডবক্স বন্ধ করার ফলে
JavaScriptIsolate-এর সব ইনস্ট্যান্সের বাকি থাকা সব মূল্যায়ন
SandboxDeadException-এর সাথে ব্যর্থ হয়। জাভাস্ক্রিপ্ট মূল্যায়ন করার সময় কোনও সমস্যা হলে, একটি JavaScriptException তৈরি করা হয়। আরও নির্দিষ্ট ব্যতিক্রমের জন্য
এর সাবক্লাস দেখুন।
স্যান্ডবক্স ক্র্যাশ ম্যানেজ করা
আপনার অ্যাপ্লিকেশনের মূল প্রসেস থেকে দূরে একটি আলাদা স্যান্ডবক্সড প্রসেসে সব জাভাস্ক্রিপ্ট চালানো হয়। জাভাস্ক্রিপ্ট কোডের কারণে এই স্যান্ডবক্সড প্রসেস ক্র্যাশ করলে, যেমন, মেমরি সীমা শেষ হয়ে যাওয়ার কারণে, অ্যাপ্লিকেশনের মূল প্রসেস প্রভাবিত হবে না।
স্যান্ডবক্স ক্র্যাশ করলে, সেই স্যান্ডবক্সের সব আইসোলেট বন্ধ হয়ে যাবে। এর সবচেয়ে
স্পষ্ট লক্ষণ হল, সব মূল্যায়ন IsolateTerminatedException-এর
সাথে ব্যর্থ হতে শুরু করবে। পরিস্থিতির উপর নির্ভর করে, আরও
নির্দিষ্ট ব্যতিক্রম যেমন SandboxDeadException বা
MemoryLimitExceededException থ্রো করা হতে পারে।
প্রতিটি আলাদা মূল্যায়নের জন্য ক্র্যাশ ম্যানেজ করা সবসময় সম্ভব হয় না।
এছাড়াও, ব্যাকগ্রাউন্ড টাস্ক বা অন্যান্য আইসোলেটে মূল্যায়নের কারণে,
স্পষ্টভাবে অনুরোধ করা মূল্যায়নের বাইরেও কোনও আইসোলেট বন্ধ হয়ে যেতে পারে। 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);
ঐচ্ছিক স্যান্ডবক্স ফিচার
অন্তর্নিহিত WebView ভার্সনের উপর নির্ভর করে, স্যান্ডবক্স প্রয়োগের ক্ষেত্রে
বিভিন্ন ধরনের ফিচার উপলভ্য থাকতে পারে। তাই, JavaScriptSandbox.isFeatureSupported(...) ব্যবহার করে প্রতিটি প্রয়োজনীয় ফিচার
কোয়েরি করা প্রয়োজন। এইসব ফিচারের উপর নির্ভর করে কলিং পদ্ধতি ব্যবহার করার আগে
ফিচারের স্ট্যাটাস চেক করা গুরুত্বপূর্ণ।
JavaScriptIsolate পদ্ধতি যা সব জায়গায় উপলভ্য নাও হতে পারে,
সেগুলি RequiresFeature অ্যানোটেশন দিয়ে অ্যানোটেট করা হয়, যাতে কোডে এই
কলগুলি সহজেই খুঁজে পাওয়া যায়।
পাসিং প্যারামিটার
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT কাজ করলে, জাভাস্ক্রিপ্ট ইঞ্জিনে পাঠানো মূল্যায়নের অনুরোধ বাইন্ডার ট্রানজ্যাকশনের সীমার
দ্বারা সীমাবদ্ধ থাকে না। ফিচারটি কাজ না করলে, JavaScriptEngine-এ সব ডেটা
Binder ট্রানজ্যাকশনের মাধ্যমে হয়। সাধারণ
ট্রানজ্যাকশন সাইজ লিমিট ডেটা পাস করে বা ডেটা ফেরত দেয় এমন প্রতিটি কলের ক্ষেত্রে প্রযোজ্য।
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT কাজ না করলে,
উত্তর সবসময় স্ট্রিং হিসেবে ফেরত পাঠানো হয় এবং
বাইন্ডার ট্রানজ্যাকশনের সর্বাধিক সাইজের সীমা
প্রযোজ্য হয়। স্ট্রিং নয় এমন ভ্যালু অবশ্যই জাভাস্ক্রিপ্ট স্ট্রিংয়ে কনভার্ট করতে হবে
অন্যথায় খালি স্ট্রিং রিটার্ন করা হয়। JS_FEATURE_PROMISE_RETURN
ফিচার কাজ করলে, জাভাস্ক্রিপ্ট কোড বিকল্প হিসেবে String-এ সমাধান করে
একটি প্রমিস ফিরিয়ে দিতে পারে।
JavaScriptIsolate ইনস্ট্যান্সে বড় বাইট অ্যারে পাস করার জন্য, আপনি
provideNamedData(...) API ব্যবহার করতে পারেন। এই API-এর ব্যবহার Binder ট্রানজ্যাকশন সীমার
দ্বারা সীমাবদ্ধ নয়। প্রতিটি বাইট অ্যারে অবশ্যই একটি অনন্য শনাক্তকারী ব্যবহার করে পাস করতে হবে যা পুনরায় ব্যবহার করা যাবে না।
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);
}
Wasm কোড রান করা হচ্ছে
provideNamedData(...) API ব্যবহার করে WebAssembly (Wasm) কোড পাস করা যেতে পারে, তারপর নিচে দেখানো পদ্ধতিতে কম্পাইল ও এক্সিকিউট করা যেতে পারে।
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);
}
JavaScriptIsolate Separation
সব JavaScriptIsolate ইনস্ট্যান্স একে অপরের থেকে স্বতন্ত্র এবং কোনও কিছু
শেয়ার করে না। নিম্নলিখিত স্নিপেটের ফলাফল হল
Hi from AAA!5
এবং
Uncaught Reference Error: a is not defined
কারণ, "jsOne"-এ তৈরি করা অবজেক্ট "jsTwo" ইনস্ট্যান্সে দেখা যায় না।
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);
Kotlin সহায়তা
Kotlin coroutines-এর সাথে এই Jetpack লাইব্রেরি ব্যবহার করতে, kotlinx-coroutines-guava-এ ডিপেন্ডেন্সি যোগ করুন। এটি
ListenableFuture-এর সাথে ইন্টিগ্রেশনের অনুমতি দেয়।
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
Jetpack লাইব্রেরি API এখন একটি কোরুটিন স্কোপ থেকে কল করা যেতে পারে, যেমন নিচে দেখানো হয়েছে:
// 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
)
}
কনফিগারেশন প্যারামিটার
আইসোলেটেড এনভায়রনমেন্ট ইনস্ট্যান্সের অনুরোধ করার সময়, আপনি এটির কনফিগারেশন
পরিবর্তন করতে পারবেন। কনফিগারেশন পরিবর্তন করতে, IsolateStartupParameters ইনস্ট্যান্স
JavaScriptSandbox.createIsolate(...)-এ পাস করুন।
বর্তমানে প্যারামিটারগুলি সর্বাধিক হিপ সাইজ এবং মূল্যায়নের রিটার্ন ভ্যালু এবং ত্রুটির জন্য সর্বাধিক সাইজ নির্দিষ্ট করার অনুমতি দেয়।