Skip to content

Commit

Permalink
[JS] Replace J2V8 based ScriptEngine with a process-based version
Browse files Browse the repository at this point in the history
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
bashor committed Dec 7, 2020
1 parent 39cc149 commit 4c69f78
Show file tree
Hide file tree
Showing 15 changed files with 356 additions and 264 deletions.
2 changes: 1 addition & 1 deletion js/js.engines/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
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 js/js.engines/src/org/jetbrains/kotlin/js/engine/ScriptEngine.kt
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) }
}
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 js/js.engines/src/org/jetbrains/kotlin/js/engine/ScriptEngineV8.kt
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) }
}
Loading

0 comments on commit 4c69f78

Please sign in to comment.