Skip to content

Commit 4c69f78

Browse files
committed
[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.
1 parent 39cc149 commit 4c69f78

File tree

15 files changed

+356
-264
lines changed

15 files changed

+356
-264
lines changed

js/js.engines/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dependencies {
88
compile(project(":compiler:util"))
99
compile(project(":js:js.ast"))
1010
compile(project(":js:js.translator"))
11-
compileOnly(intellijCoreDep()) { includeJars("intellij-core") }
11+
compile(intellijCoreDep()) { includeJars("intellij-core") }
1212
}
1313

1414
sourceSets {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
4+
*/
5+
6+
package org.jetbrains.kotlin.js.engine
7+
8+
import com.intellij.openapi.util.text.StringUtil
9+
10+
private val LINE_SEPARATOR = System.getProperty("line.separator")!!
11+
private val END_MARKER = "<END>$LINE_SEPARATOR"
12+
13+
abstract class ProcessBasedScriptEngine(
14+
private val executablePath: String
15+
) : ScriptEngine {
16+
17+
private var process: Process? = null
18+
private val buffer = ByteArray(1024)
19+
20+
override fun eval(script: String): String {
21+
val vm = getOrCreateProcess()
22+
23+
val stdin = vm.outputStream
24+
val stdout = vm.inputStream
25+
val stderr = vm.errorStream
26+
27+
val writer = stdin.writer()
28+
writer.write(StringUtil.convertLineSeparators(script, "\\n") + "\n")
29+
writer.flush()
30+
31+
val out = StringBuilder()
32+
33+
while (vm.isAlive) {
34+
val n = stdout.available()
35+
if (n == 0) continue
36+
37+
val count = stdout.read(buffer)
38+
39+
val s = String(buffer, 0, count)
40+
out.append(s)
41+
42+
if (out.endsWith(END_MARKER)) break
43+
}
44+
45+
if (stderr.available() > 0) {
46+
val err = StringBuilder()
47+
48+
while (vm.isAlive && stderr.available() > 0) {
49+
val count = stderr.read(buffer)
50+
val s = String(buffer, 0, count)
51+
err.append(s)
52+
}
53+
54+
error("ERROR:\n$err\nOUTPUT:\n$out")
55+
}
56+
57+
return out.removeSuffix(END_MARKER).removeSuffix(LINE_SEPARATOR).toString()
58+
}
59+
60+
override fun loadFile(path: String) {
61+
eval("load('${path.replace('\\', '/')}');")
62+
}
63+
64+
override fun reset() {
65+
eval("!reset")
66+
}
67+
68+
override fun saveGlobalState() {
69+
eval("!saveGlobalState")
70+
}
71+
72+
override fun restoreGlobalState() {
73+
eval("!restoreGlobalState")
74+
}
75+
76+
override fun release() {
77+
process?.destroy()
78+
process = null
79+
}
80+
81+
private fun getOrCreateProcess(): Process {
82+
val p = process
83+
84+
if (p != null && p.isAlive) return p
85+
86+
process = null
87+
88+
val builder = ProcessBuilder(
89+
executablePath,
90+
"js/js.engines/src/org/jetbrains/kotlin/js/engine/repl.js",
91+
)
92+
return builder.start().also {
93+
process = it
94+
}
95+
}
96+
}
Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,48 @@
11
/*
2-
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
2+
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
33
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
44
*/
55

66
package org.jetbrains.kotlin.js.engine
77

88
interface ScriptEngine {
9-
fun <T> eval(script: String): T
10-
fun evalVoid(script: String)
11-
fun <T> callMethod(obj: Any, name: String, vararg args: Any?): T
9+
fun eval(script: String): String
10+
11+
// TODO Add API to load few files at once?
1212
fun loadFile(path: String)
13+
14+
/**
15+
* Performs truly reset of the engine state.
16+
* */
17+
fun reset()
18+
19+
/**
20+
* Saves current state of global object.
21+
*
22+
* See also [restoreGlobalState]
23+
*/
24+
fun saveGlobalState()
25+
26+
/**
27+
* Restores global object from the last saved state.
28+
*
29+
* See also [saveGlobalState]
30+
*/
31+
fun restoreGlobalState()
32+
33+
34+
/**
35+
* Release held resources.
36+
*
37+
* Must be called explicitly before an object is garbage collected to avoid leaking resources.
38+
*/
1339
fun release()
14-
fun <T> releaseObject(t: T)
40+
}
41+
42+
interface ScriptEngineWithTypedResult : ScriptEngine {
43+
fun <R> evalWithTypedResult(script: String): R
44+
}
1545

16-
fun saveState()
17-
fun restoreState()
18-
}
46+
fun ScriptEngine.loadFiles(files: List<String>) {
47+
files.forEach { loadFile(it) }
48+
}
Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,49 @@
11
/*
2-
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
2+
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
33
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
44
*/
55
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
66
package org.jetbrains.kotlin.js.engine
77

8+
import jdk.nashorn.api.scripting.NashornScriptEngine
89
import jdk.nashorn.api.scripting.NashornScriptEngineFactory
910
import jdk.nashorn.internal.runtime.ScriptRuntime
10-
import javax.script.Invocable
1111

12-
class ScriptEngineNashorn : ScriptEngine {
12+
class ScriptEngineNashorn : ScriptEngineWithTypedResult {
1313
private var savedState: Map<String, Any?>? = null
1414

1515
// TODO use "-strict"
1616
private val myEngine = NashornScriptEngineFactory().getScriptEngine("--language=es5", "--no-java", "--no-syntax-extensions")
1717

18-
@Suppress("UNCHECKED_CAST")
19-
override fun <T> eval(script: String): T {
20-
return myEngine.eval(script) as T
21-
}
22-
23-
override fun evalVoid(script: String) {
24-
myEngine.eval(script)
25-
}
18+
override fun eval(script: String): String = evalWithTypedResult<Any?>(script).toString()
2619

2720
@Suppress("UNCHECKED_CAST")
28-
override fun <T> callMethod(obj: Any, name: String, vararg args: Any?): T {
29-
return (myEngine as Invocable).invokeMethod(obj, name, *args) as T
21+
override fun <R> evalWithTypedResult(script: String): R {
22+
return myEngine.eval(script) as R
3023
}
3124

3225
override fun loadFile(path: String) {
33-
evalVoid("load('${path.replace('\\', '/')}');")
26+
eval("load('${path.replace('\\', '/')}');")
3427
}
3528

36-
override fun release() {}
37-
override fun <T> releaseObject(t: T) {}
38-
29+
override fun reset() {
30+
throw UnsupportedOperationException()
31+
}
3932

40-
private fun getGlobalState(): MutableMap<String, Any?> = eval("this")
33+
private fun getGlobalState(): MutableMap<String, Any?> = evalWithTypedResult("this")
4134

42-
override fun saveState() {
35+
override fun saveGlobalState() {
4336
savedState = getGlobalState().toMap()
4437
}
4538

46-
override fun restoreState() {
39+
override fun restoreGlobalState() {
4740
val globalState = getGlobalState()
4841
val originalState = savedState!!
4942
for (key in globalState.keys) {
5043
globalState[key] = originalState[key] ?: ScriptRuntime.UNDEFINED
5144
}
5245
}
53-
}
46+
47+
override fun release() {
48+
}
49+
}
Lines changed: 14 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,23 @@
11
/*
2-
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
2+
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
33
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
44
*/
55

66
package org.jetbrains.kotlin.js.engine
77

8-
import com.eclipsesource.v8.V8
9-
import com.eclipsesource.v8.V8Array
10-
import com.eclipsesource.v8.V8Object
11-
import com.eclipsesource.v8.utils.V8ObjectUtils
12-
import java.io.File
13-
14-
class ScriptEngineV8(LIBRARY_PATH_BASE: String) : ScriptEngine {
15-
16-
override fun <T> releaseObject(t: T) {
17-
(t as? V8Object)?.release()
18-
}
19-
20-
private var savedState: List<String>? = null
21-
22-
override fun restoreState() {
23-
val scriptBuilder = StringBuilder()
24-
25-
val globalState = getGlobalPropertyNames()
26-
val originalState = savedState!!
27-
for (key in globalState) {
28-
if (key !in originalState) {
29-
scriptBuilder.append("this['$key'] = void 0;\n")
30-
}
8+
class ScriptEngineV8 : ProcessBasedScriptEngine(System.getProperty("javascript.engine.path.V8"))
9+
10+
fun main() {
11+
// System.setProperty("javascript.engine.path.V8", "<path-to-d8>")
12+
val vm = ScriptEngineV8()
13+
println("Welcome!")
14+
while (true) {
15+
print("> ")
16+
val t = readLine()
17+
try {
18+
println(vm.eval(t!!))
19+
} catch (e: Throwable) {
20+
System.err.println(e)
3121
}
32-
evalVoid(scriptBuilder.toString())
33-
}
34-
35-
private fun getGlobalPropertyNames(): List<String> {
36-
val v8Array = eval<V8Array>("Object.getOwnPropertyNames(this)")
37-
@Suppress("UNCHECKED_CAST") val javaArray = V8ObjectUtils.toList(v8Array) as List<String>
38-
v8Array.release()
39-
return javaArray
40-
}
41-
42-
override fun saveState() {
43-
if (savedState == null) {
44-
savedState = getGlobalPropertyNames()
45-
}
46-
}
47-
48-
private val myRuntime: V8 = V8.createV8Runtime("global", LIBRARY_PATH_BASE)
49-
50-
@Suppress("UNCHECKED_CAST")
51-
override fun <T> eval(script: String): T {
52-
return myRuntime.executeScript(script) as T
53-
}
54-
55-
override fun evalVoid(script: String) {
56-
return myRuntime.executeVoidScript(script)
57-
}
58-
59-
@Suppress("UNCHECKED_CAST")
60-
override fun <T> callMethod(obj: Any, name: String, vararg args: Any?): T {
61-
if (obj !is V8Object) {
62-
throw Exception("InteropV8 can deal only with V8Object")
63-
}
64-
65-
val runtimeArray = V8Array(myRuntime)
66-
val result = obj.executeFunction(name, runtimeArray) as T
67-
runtimeArray.release()
68-
return result
69-
}
70-
71-
override fun loadFile(path: String) {
72-
myRuntime.executeVoidScript(File(path).bufferedReader().use { it.readText() }, path, 0)
73-
}
74-
75-
override fun release() {
76-
myRuntime.release()
7722
}
7823
}
79-
80-
class ScriptEngineV8Lazy(LIBRARY_PATH_BASE: String) : ScriptEngine {
81-
override fun <T> eval(script: String) = engine.eval<T>(script)
82-
83-
override fun saveState() = engine.saveState()
84-
85-
override fun evalVoid(script: String) = engine.evalVoid(script)
86-
87-
override fun <T> callMethod(obj: Any, name: String, vararg args: Any?) = engine.callMethod<T>(obj, name, args)
88-
89-
override fun loadFile(path: String) = engine.loadFile(path)
90-
91-
override fun release() = engine.release()
92-
93-
override fun <T> releaseObject(t: T) = engine.releaseObject(t)
94-
95-
override fun restoreState() = engine.restoreState()
96-
97-
private val engine by lazy { ScriptEngineV8(LIBRARY_PATH_BASE) }
98-
}

0 commit comments

Comments
 (0)