-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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.
- Loading branch information
Showing
15 changed files
with
356 additions
and
264 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
js/js.engines/src/org/jetbrains/kotlin/js/engine/ProcessBasedScriptEngine.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
} |
46 changes: 38 additions & 8 deletions
46
js/js.engines/src/org/jetbrains/kotlin/js/engine/ScriptEngine.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 eval(script: String): String | ||
|
||
// 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() | ||
fun <T> releaseObject(t: T) | ||
} | ||
|
||
interface ScriptEngineWithTypedResult : ScriptEngine { | ||
fun <R> evalWithTypedResult(script: String): R | ||
} | ||
|
||
fun saveState() | ||
fun restoreState() | ||
} | ||
fun ScriptEngine.loadFiles(files: List<String>) { | ||
files.forEach { loadFile(it) } | ||
} |
38 changes: 17 additions & 21 deletions
38
js/js.engines/src/org/jetbrains/kotlin/js/engine/ScriptEngineNashorn.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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?> = eval("this") | ||
private fun getGlobalState(): MutableMap<String, Any?> = evalWithTypedResult("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() { | ||
} | ||
} |
103 changes: 14 additions & 89 deletions
103
js/js.engines/src/org/jetbrains/kotlin/js/engine/ScriptEngineV8.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(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") | ||
} | ||
class ScriptEngineV8 : ProcessBasedScriptEngine(System.getProperty("javascript.engine.path.V8")) | ||
|
||
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) } | ||
} |
Oops, something went wrong.