[JS] Replace J2V8 based ScriptEngine with a process-based version
The main advantage of this is that we can use a newer and official build of V8. Also, with new infra, we can use other JS engines. Other changes: * ScriptEngine API is simplified and documented. * Introduce ScriptEngineWithTypedResult with typed `eval`, mostly for JsReplEvaluator and JsScriptEvaluator. * J2V8 version is completely removed. * Use new ScriptEngineV8 everywhere by default. * System property `kotlin.js.useNashorn` switches to Nashorn in all tests.
This commit is contained in:
@@ -8,7 +8,7 @@ dependencies {
|
||||
compile(project(":compiler:util"))
|
||||
compile(project(":js:js.ast"))
|
||||
compile(project(":js:js.translator"))
|
||||
compileOnly(intellijCoreDep()) { includeJars("intellij-core") }
|
||||
compile(intellijCoreDep()) { includeJars("intellij-core") }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.js.engine
|
||||
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
|
||||
private val LINE_SEPARATOR = System.getProperty("line.separator")!!
|
||||
private val END_MARKER = "<END>$LINE_SEPARATOR"
|
||||
|
||||
abstract class ProcessBasedScriptEngine(
|
||||
private val executablePath: String
|
||||
) : ScriptEngine {
|
||||
|
||||
private var process: Process? = null
|
||||
private val buffer = ByteArray(1024)
|
||||
|
||||
override fun eval(script: String): String {
|
||||
val vm = getOrCreateProcess()
|
||||
|
||||
val stdin = vm.outputStream
|
||||
val stdout = vm.inputStream
|
||||
val stderr = vm.errorStream
|
||||
|
||||
val writer = stdin.writer()
|
||||
writer.write(StringUtil.convertLineSeparators(script, "\\n") + "\n")
|
||||
writer.flush()
|
||||
|
||||
val out = StringBuilder()
|
||||
|
||||
while (vm.isAlive) {
|
||||
val n = stdout.available()
|
||||
if (n == 0) continue
|
||||
|
||||
val count = stdout.read(buffer)
|
||||
|
||||
val s = String(buffer, 0, count)
|
||||
out.append(s)
|
||||
|
||||
if (out.endsWith(END_MARKER)) break
|
||||
}
|
||||
|
||||
if (stderr.available() > 0) {
|
||||
val err = StringBuilder()
|
||||
|
||||
while (vm.isAlive && stderr.available() > 0) {
|
||||
val count = stderr.read(buffer)
|
||||
val s = String(buffer, 0, count)
|
||||
err.append(s)
|
||||
}
|
||||
|
||||
error("ERROR:\n$err\nOUTPUT:\n$out")
|
||||
}
|
||||
|
||||
return out.removeSuffix(END_MARKER).removeSuffix(LINE_SEPARATOR).toString()
|
||||
}
|
||||
|
||||
override fun loadFile(path: String) {
|
||||
eval("load('${path.replace('\\', '/')}');")
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
eval("!reset")
|
||||
}
|
||||
|
||||
override fun saveGlobalState() {
|
||||
eval("!saveGlobalState")
|
||||
}
|
||||
|
||||
override fun restoreGlobalState() {
|
||||
eval("!restoreGlobalState")
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
process?.destroy()
|
||||
process = null
|
||||
}
|
||||
|
||||
private fun getOrCreateProcess(): Process {
|
||||
val p = process
|
||||
|
||||
if (p != null && p.isAlive) return p
|
||||
|
||||
process = null
|
||||
|
||||
val builder = ProcessBuilder(
|
||||
executablePath,
|
||||
"js/js.engines/src/org/jetbrains/kotlin/js/engine/repl.js",
|
||||
)
|
||||
return builder.start().also {
|
||||
process = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,48 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.js.engine
|
||||
|
||||
interface ScriptEngine {
|
||||
fun <T> eval(script: String): T
|
||||
fun evalVoid(script: String)
|
||||
fun <T> callMethod(obj: Any, name: String, vararg args: Any?): T
|
||||
fun loadFile(path: String)
|
||||
fun release()
|
||||
fun <T> releaseObject(t: T)
|
||||
fun eval(script: String): String
|
||||
|
||||
fun saveState()
|
||||
fun restoreState()
|
||||
}
|
||||
// TODO Add API to load few files at once?
|
||||
fun loadFile(path: String)
|
||||
|
||||
/**
|
||||
* Performs truly reset of the engine state.
|
||||
* */
|
||||
fun reset()
|
||||
|
||||
/**
|
||||
* Saves current state of global object.
|
||||
*
|
||||
* See also [restoreGlobalState]
|
||||
*/
|
||||
fun saveGlobalState()
|
||||
|
||||
/**
|
||||
* Restores global object from the last saved state.
|
||||
*
|
||||
* See also [saveGlobalState]
|
||||
*/
|
||||
fun restoreGlobalState()
|
||||
|
||||
|
||||
/**
|
||||
* Release held resources.
|
||||
*
|
||||
* Must be called explicitly before an object is garbage collected to avoid leaking resources.
|
||||
*/
|
||||
fun release()
|
||||
}
|
||||
|
||||
interface ScriptEngineWithTypedResult : ScriptEngine {
|
||||
fun <R> evalWithTypedResult(script: String): R
|
||||
}
|
||||
|
||||
fun ScriptEngine.loadFiles(files: List<String>) {
|
||||
files.forEach { loadFile(it) }
|
||||
}
|
||||
|
||||
@@ -1,53 +1,49 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
|
||||
package org.jetbrains.kotlin.js.engine
|
||||
|
||||
import jdk.nashorn.api.scripting.NashornScriptEngine
|
||||
import jdk.nashorn.api.scripting.NashornScriptEngineFactory
|
||||
import jdk.nashorn.internal.runtime.ScriptRuntime
|
||||
import javax.script.Invocable
|
||||
|
||||
class ScriptEngineNashorn : ScriptEngine {
|
||||
class ScriptEngineNashorn : ScriptEngineWithTypedResult {
|
||||
private var savedState: Map<String, Any?>? = null
|
||||
|
||||
// TODO use "-strict"
|
||||
private val myEngine = NashornScriptEngineFactory().getScriptEngine("--language=es5", "--no-java", "--no-syntax-extensions")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> eval(script: String): T {
|
||||
return myEngine.eval(script) as T
|
||||
}
|
||||
|
||||
override fun evalVoid(script: String) {
|
||||
myEngine.eval(script)
|
||||
}
|
||||
override fun eval(script: String): String = evalWithTypedResult<Any?>(script).toString()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> callMethod(obj: Any, name: String, vararg args: Any?): T {
|
||||
return (myEngine as Invocable).invokeMethod(obj, name, *args) as T
|
||||
override fun <R> evalWithTypedResult(script: String): R {
|
||||
return myEngine.eval(script) as R
|
||||
}
|
||||
|
||||
override fun loadFile(path: String) {
|
||||
evalVoid("load('${path.replace('\\', '/')}');")
|
||||
eval("load('${path.replace('\\', '/')}');")
|
||||
}
|
||||
|
||||
override fun release() {}
|
||||
override fun <T> releaseObject(t: T) {}
|
||||
override fun reset() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun getGlobalState(): MutableMap<String, Any?> = evalWithTypedResult("this")
|
||||
|
||||
private fun getGlobalState(): MutableMap<String, Any?> = eval("this")
|
||||
|
||||
override fun saveState() {
|
||||
override fun saveGlobalState() {
|
||||
savedState = getGlobalState().toMap()
|
||||
}
|
||||
|
||||
override fun restoreState() {
|
||||
override fun restoreGlobalState() {
|
||||
val globalState = getGlobalState()
|
||||
val originalState = savedState!!
|
||||
for (key in globalState.keys) {
|
||||
globalState[key] = originalState[key] ?: ScriptRuntime.UNDEFINED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +1,23 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.js.engine
|
||||
|
||||
import com.eclipsesource.v8.V8
|
||||
import com.eclipsesource.v8.V8Array
|
||||
import com.eclipsesource.v8.V8Object
|
||||
import com.eclipsesource.v8.utils.V8ObjectUtils
|
||||
import java.io.File
|
||||
class ScriptEngineV8 : ProcessBasedScriptEngine(System.getProperty("javascript.engine.path.V8"))
|
||||
|
||||
class ScriptEngineV8(LIBRARY_PATH_BASE: String) : ScriptEngine {
|
||||
|
||||
override fun <T> releaseObject(t: T) {
|
||||
(t as? V8Object)?.release()
|
||||
}
|
||||
|
||||
private var savedState: List<String>? = null
|
||||
|
||||
override fun restoreState() {
|
||||
val scriptBuilder = StringBuilder()
|
||||
|
||||
val globalState = getGlobalPropertyNames()
|
||||
val originalState = savedState!!
|
||||
for (key in globalState) {
|
||||
if (key !in originalState) {
|
||||
scriptBuilder.append("this['$key'] = void 0;\n")
|
||||
}
|
||||
fun main() {
|
||||
// System.setProperty("javascript.engine.path.V8", "<path-to-d8>")
|
||||
val vm = ScriptEngineV8()
|
||||
println("Welcome!")
|
||||
while (true) {
|
||||
print("> ")
|
||||
val t = readLine()
|
||||
try {
|
||||
println(vm.eval(t!!))
|
||||
} catch (e: Throwable) {
|
||||
System.err.println(e)
|
||||
}
|
||||
evalVoid(scriptBuilder.toString())
|
||||
}
|
||||
|
||||
private fun getGlobalPropertyNames(): List<String> {
|
||||
val v8Array = eval<V8Array>("Object.getOwnPropertyNames(this)")
|
||||
@Suppress("UNCHECKED_CAST") val javaArray = V8ObjectUtils.toList(v8Array) as List<String>
|
||||
v8Array.release()
|
||||
return javaArray
|
||||
}
|
||||
|
||||
override fun saveState() {
|
||||
if (savedState == null) {
|
||||
savedState = getGlobalPropertyNames()
|
||||
}
|
||||
}
|
||||
|
||||
private val myRuntime: V8 = V8.createV8Runtime("global", LIBRARY_PATH_BASE)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> eval(script: String): T {
|
||||
return myRuntime.executeScript(script) as T
|
||||
}
|
||||
|
||||
override fun evalVoid(script: String) {
|
||||
return myRuntime.executeVoidScript(script)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> callMethod(obj: Any, name: String, vararg args: Any?): T {
|
||||
if (obj !is V8Object) {
|
||||
throw Exception("InteropV8 can deal only with V8Object")
|
||||
}
|
||||
|
||||
val runtimeArray = V8Array(myRuntime)
|
||||
val result = obj.executeFunction(name, runtimeArray) as T
|
||||
runtimeArray.release()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun loadFile(path: String) {
|
||||
myRuntime.executeVoidScript(File(path).bufferedReader().use { it.readText() }, path, 0)
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
myRuntime.release()
|
||||
}
|
||||
}
|
||||
|
||||
class ScriptEngineV8Lazy(LIBRARY_PATH_BASE: String) : ScriptEngine {
|
||||
override fun <T> eval(script: String) = engine.eval<T>(script)
|
||||
|
||||
override fun saveState() = engine.saveState()
|
||||
|
||||
override fun evalVoid(script: String) = engine.evalVoid(script)
|
||||
|
||||
override fun <T> callMethod(obj: Any, name: String, vararg args: Any?) = engine.callMethod<T>(obj, name, args)
|
||||
|
||||
override fun loadFile(path: String) = engine.loadFile(path)
|
||||
|
||||
override fun release() = engine.release()
|
||||
|
||||
override fun <T> releaseObject(t: T) = engine.releaseObject(t)
|
||||
|
||||
override fun restoreState() = engine.restoreState()
|
||||
|
||||
private val engine by lazy { ScriptEngineV8(LIBRARY_PATH_BASE) }
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
// <path-to-v8> js/js.engines/src/org/jetbrains/kotlin/js/engine/repl.js
|
||||
|
||||
/*
|
||||
Some of non-standard APIs available in standalone JS engines:
|
||||
|
||||
v8 sm jsc
|
||||
load + + + load and evaluate a file
|
||||
print + + + print to stdout
|
||||
printErr + + + print to stderr
|
||||
read + + + read a file as a text (v8, sm, jsc) or binary (sm, jsc)
|
||||
readline + + + read line from stdin
|
||||
readbuffer + - - read a binary file in v8
|
||||
quit + + + stop the process
|
||||
|
||||
V8:
|
||||
https://v8.dev/docs/d8
|
||||
https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/d8.cc
|
||||
https://riptutorial.com/v8/example/25393/useful-built-in-functions-and-objects-in-d8
|
||||
|
||||
SpiderMonkey:
|
||||
https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Introduction_to_the_JavaScript_shell
|
||||
|
||||
JavaScriptCore:
|
||||
https://trac.webkit.org/wiki/JSC
|
||||
https://github.com/WebKit/webkit/blob/master/Source/JavaScriptCore/jsc.cpp
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
let currentRealmIndex = Realm.current();
|
||||
|
||||
function resetRealm() {
|
||||
if (currentRealmIndex !== 0) Realm.dispose(currentRealmIndex);
|
||||
currentRealmIndex = Realm.createAllowCrossRealmAccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {?Map<string, ?any>}
|
||||
*/
|
||||
let globalState = null;
|
||||
|
||||
function saveGlobalState() {
|
||||
globalState = new Map();
|
||||
const currentGlobal = Realm.global(currentRealmIndex)
|
||||
for (const k in currentGlobal) {
|
||||
globalState.set(k, currentGlobal[k]);
|
||||
}
|
||||
|
||||
console.log(Array.from(globalState.entries()))
|
||||
}
|
||||
|
||||
function restoreGlobalState() {
|
||||
if (globalState === null) throw Error("There is no saved state!")
|
||||
|
||||
const currentGlobal = Realm.global(currentRealmIndex)
|
||||
|
||||
console.log(Array.from(globalState.entries()))
|
||||
|
||||
for (const k in currentGlobal) {
|
||||
let prev = globalState.get(k);
|
||||
if (prev !== currentGlobal[k]) {
|
||||
currentGlobal[k] = prev;
|
||||
}
|
||||
}
|
||||
globalState = null;
|
||||
}
|
||||
|
||||
// To prevent accessing to current global state
|
||||
resetRealm();
|
||||
|
||||
// noinspection InfiniteLoopJS
|
||||
while (true) {
|
||||
let code = readline().replace(/\\n/g, '\n');
|
||||
|
||||
try {
|
||||
switch (code) {
|
||||
case "!reset":
|
||||
resetRealm()
|
||||
break;
|
||||
case "!saveGlobalState":
|
||||
saveGlobalState();
|
||||
break;
|
||||
case "!restoreGlobalState":
|
||||
restoreGlobalState();
|
||||
break;
|
||||
default:
|
||||
print(Realm.eval(currentRealmIndex, code));
|
||||
}
|
||||
} catch(e) {
|
||||
printErr(e.stack != null ? e.stack : e.toString());
|
||||
printErr('\nCODE:\n' + code);
|
||||
}
|
||||
|
||||
print('<END>');
|
||||
}
|
||||
Reference in New Issue
Block a user