Skip to content

Instantly share code, notes, and snippets.

@jirkamarsik
Created July 23, 2019 11:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jirkamarsik/b282fef51075468b735105ede1c25452 to your computer and use it in GitHub Desktop.
Save jirkamarsik/b282fef51075468b735105ede1c25452 to your computer and use it in GitHub Desktop.
Porting Talkyard from Nashorn to GraalVM JavaScript
diff --git a/app/debiki/Nashorn.scala b/app/debiki/Nashorn.scala
index 13f9f2eb4..bdc2604a7 100644
--- a/app/debiki/Nashorn.scala
+++ b/app/debiki/Nashorn.scala
@@ -22,8 +22,7 @@ import java.io.{BufferedWriter, FileWriter}
import com.debiki.core._
import com.debiki.core.Prelude._
import java.{io => jio}
-
-import javax.{script => js}
+import org.graalvm.{polyglot => graalvm}
import debiki.onebox.{InstantOneboxRendererForNashorn, Onebox}
import org.apache.lucene.util.IOUtils
import play.api.Play
@@ -31,7 +30,6 @@ import play.api.Play
import scala.concurrent.Future
import scala.util.Try
import Nashorn._
-import jdk.nashorn.api.scripting.ScriptObjectMirror
import org.scalactic.{Bad, ErrorMessage, Good, Or}
import scala.collection.mutable.ArrayBuffer
@@ -62,14 +60,9 @@ class Nashorn(globals: Globals) {
/** The Nashorn Javascript engine isn't thread safe. */
private val javascriptEngines =
- new java.util.concurrent.LinkedBlockingDeque[js.ScriptEngine](999)
+ new java.util.concurrent.LinkedBlockingDeque[Option[graalvm.Context]](999)
- private val BrokenEngine = new js.AbstractScriptEngine() {
- override def eval(script: String, context: js.ScriptContext): AnyRef = null
- override def eval(reader: jio.Reader, context: js.ScriptContext): AnyRef = null
- override def getFactory: js.ScriptEngineFactory = null
- override def createBindings(): js.Bindings = null
- }
+ private val BrokenEngine = None
@volatile private var firstCreateEngineError: Option[Throwable] = None
@@ -180,7 +173,7 @@ class Nashorn(globals: Globals) {
}
return
}
- javascriptEngines.putLast(engine)
+ javascriptEngines.putLast(Some(engine))
}
@@ -193,12 +186,13 @@ class Nashorn(globals: Globals) {
}
- private def renderPageImpl[R](engine: js.Invocable, reactStoreJsonString: String)
+ private def renderPageImpl[R](engine: graalvm.Context, reactStoreJsonString: String)
: String Or ErrorMessage = {
val timeBefore = System.nanoTime()
- val htmlOrError = engine.invokeFunction(
- "renderReactServerSide", reactStoreJsonString, cdnOrigin.getOrElse("")).asInstanceOf[String]
+
+ val htmlOrError = engine.getBindings("js").getMember("renderReactServerSide")
+ .execute(reactStoreJsonString, cdnOrigin.getOrElse("")).asString
if (htmlOrError.startsWith(ErrorRenderingReact)) {
logger.error(s"Error rendering page with React server side [DwE5KGW2]")
return Bad(htmlOrError)
@@ -232,44 +226,41 @@ class Nashorn(globals: Globals) {
// The onebox renderer needs a Javascript engine to sanitize html (via Caja JsHtmlSanitizer)
// and we'll reuse `engine` so we won't have to create any additional engine.
oneboxRenderer.javascriptEngine = Some(engine)
- val resultObj: Object = engine.invokeFunction("renderAndSanitizeCommonMark", commonMarkSource,
+ val result: graalvm.Value = engine.getBindings("js").getMember("renderAndSanitizeCommonMark").execute(
+ commonMarkSource,
true.asInstanceOf[Object], // allowClassIdDataAttrs.asInstanceOf[Object],
followLinks.asInstanceOf[Object],
- oneboxRenderer, uploadsUrlPrefix)
+ oneboxRenderer,
+ uploadsUrlPrefix)
oneboxRenderer.javascriptEngine = None
- val result: ScriptObjectMirror = resultObj match {
- case scriptObjectMirror: ScriptObjectMirror =>
- scriptObjectMirror
- case errorDetails: ErrorMessage =>
- // Don't use Die — the stack trace to here isn't interesting? Instead, it's the
- // errorDetails from the inside-Nashorn exception that matters.
- debiki.EdHttp.throwInternalError(
- "TyERCMEX", "Error rendering CommonMark, server side in Nashorn", errorDetails)
- case unknown =>
- die("TyERCMR01", s"Bad class: ${classNameOf(unknown)}, thing as string: ``$unknown''")
+ if (result.isString) {
+ // Don't use Die — the stack trace to here isn't interesting? Instead, it's the
+ // errorDetails from the inside-Nashorn exception that matters.
+ debiki.EdHttp.throwInternalError(
+ "TyERCMEX", "Error rendering CommonMark, server side in Nashorn", result.asString)
+ } else if (!result.hasMembers) {
+ die("TyERCMR01", s"Bad class: ${classNameOf(result)}, thing as string: ``$result''")
}
- dieIf(!result.isArray, "TyERCMR02", "Not an array")
- dieIf(!result.hasSlot(0), "TyERCMR03A", "No slot 0")
- dieIf(!result.hasSlot(1), "TyERCMR03B", "No slot 1")
- dieIf(result.hasSlot(2), "TyERCMR03C", "Has slot 2")
+ dieIf(!result.hasArrayElements, "TyERCMR02", "Not an array")
+ dieIf(result.getArraySize != 2, "TyERCMR03D", "Array of size != 2")
- val elem0 = result.getSlot(0)
- dieIf(!elem0.isInstanceOf[String], "TyERCMR04", s"Bad safeHtml class: ${classNameOf(elem0)}")
- val safeHtml = elem0.asInstanceOf[String]
+ val elem0 = result.getArrayElement(0)
+ dieIf(!elem0.isString, "TyERCMR04", s"Bad safeHtml class: ${classNameOf(elem0)}")
+ val safeHtml = elem0.asString
- val elem1 = result.getSlot(1)
- dieIf(!elem1.isInstanceOf[ScriptObjectMirror],
+ val elem1 = result.getArrayElement(1)
+ dieIf(!elem1.hasArrayElements,
"TyERCMR05", s"Bad mentionsArray class: ${classNameOf(elem1)}")
- val mentionsArrayMirror = elem1.asInstanceOf[ScriptObjectMirror]
+ val mentionsArrayMirror = elem1
val mentions = ArrayBuffer[String]()
var nextSlotIx = 0
- while (mentionsArrayMirror.hasSlot(nextSlotIx)) {
- val elem = mentionsArrayMirror.getSlot(nextSlotIx)
- dieIf(!elem.isInstanceOf[String], "TyERCMR06", s"Bad mention class: ${classNameOf(elem)}")
- mentions.append(elem.asInstanceOf[String])
+ while (nextSlotIx < mentionsArrayMirror.getArraySize) {
+ val elem = mentionsArrayMirror.getArrayElement(nextSlotIx)
+ dieIf(!elem.isString, "TyERCMR06", s"Bad mention class: ${classNameOf(elem)}")
+ mentions.append(elem.asString)
nextSlotIx += 1
}
@@ -289,13 +280,13 @@ class Nashorn(globals: Globals) {
def sanitizeHtmlReuseEngine(text: String, followLinks: Boolean,
- javascriptEngine: Option[js.Invocable]): String = {
+ javascriptEngine: Option[graalvm.Context]): String = {
if (isTestSoDisableScripts)
return "Scripts disabled [EsM44GY0]"
- def sanitizeWith(engine: js.Invocable): String = {
- val safeHtml = engine.invokeFunction(
- "sanitizeHtmlServerSide", text, followLinks.asInstanceOf[Object])
- safeHtml.asInstanceOf[String]
+ def sanitizeWith(engine: graalvm.Context): String = {
+ val safeHtml = engine.getBindings("js").getMember("sanitizeHtmlServerSide")
+ .execute(text, followLinks.asInstanceOf[Object])
+ safeHtml.asString
}
javascriptEngine match {
case Some(engine) =>
@@ -312,13 +303,13 @@ class Nashorn(globals: Globals) {
if (isTestSoDisableScripts)
return "scripts-disabled-EsM28WXP45"
withJavascriptEngine(engine => {
- val slug = engine.invokeFunction("debikiSlugify", title)
- slug.asInstanceOf[String]
+ val slug = engine.getBindings("js").getMember("debikiSlugify").execute(title)
+ slug.asString
})
}
- private def withJavascriptEngine[R](fn: (js.Invocable) => R): R = {
+ private def withJavascriptEngine[R](fn: (graalvm.Context) => R): R = {
dieIf(isTestSoDisableScripts, "EsE4YUGw")
def threadId = Thread.currentThread.getId
@@ -337,29 +328,32 @@ class Nashorn(globals: Globals) {
// Don't: Future { createOneMoreJavascriptEngine() }
}
- val engine = javascriptEngines.takeFirst()
+ val maybeEngine = javascriptEngines.takeFirst()
if (mightBlock) {
logger.debug(s"...Thread $threadName (id $threadId) got a JS engine.")
- if (engine eq BrokenEngine) {
+ if (maybeEngine eq BrokenEngine) {
logger.debug(s"...But it is broken; I'll throw an error. [DwE4KEWV52]")
}
}
- if (engine eq BrokenEngine) {
- javascriptEngines.addFirst(engine)
- die("DwE5KGF8", "Could not create Javascript engine; cannot render page",
- firstCreateEngineError getOrDie "DwE4KEW20")
- }
-
- try fn(engine.asInstanceOf[js.Invocable])
- finally {
- javascriptEngines.addFirst(engine)
+ maybeEngine match {
+ case Some(engine) => {
+ try fn(engine)
+ finally {
+ javascriptEngines.addFirst(maybeEngine)
+ }
+ }
+ case None => {
+ javascriptEngines.addFirst(maybeEngine)
+ die("DwE5KGF8", "Could not create Javascript engine; cannot render page",
+ firstCreateEngineError getOrDie "DwE4KEW20")
+ }
}
}
- private def makeJavascriptEngine(): js.ScriptEngine = {
+ private def makeJavascriptEngine(): graalvm.Context = {
val timeBefore = System.currentTimeMillis()
def threadId = java.lang.Thread.currentThread.getId
def threadName = java.lang.Thread.currentThread.getName
@@ -370,7 +364,7 @@ class Nashorn(globals: Globals) {
// Pass 'null' so that a class loader that finds the Nashorn extension will be used.
// Otherwise the Nashorn engine won't be found and `newEngine` will be null.
// See: https://github.com/playframework/playframework/issues/2532
- val newEngine = new js.ScriptEngineManager(null).getEngineByName("nashorn")
+ val newEngine = graalvm.Context.newBuilder("js").allowAllAccess(true).build
val scriptBuilder = new StringBuilder
scriptBuilder.append(i"""
@@ -502,7 +496,8 @@ class Nashorn(globals: Globals) {
}
}
- newEngine.eval(script)
+ val scriptSource = graalvm.Source.newBuilder("js", script, "setup").buildLiteral
+ newEngine.eval(scriptSource)
def timeElapsed = System.currentTimeMillis() - timeBefore
logger.debug(o"""... Done initializing Nashorn engine, took: $timeElapsed ms,
@@ -534,13 +529,13 @@ class Nashorn(globals: Globals) {
}
- private def warmupJavascriptEngine(engine: js.ScriptEngine) {
+ private def warmupJavascriptEngine(engine: graalvm.Context) {
logger.debug(o"""Warming up Nashorn engine...""")
val timeBefore = System.currentTimeMillis()
// Warming up with three laps seems enough, almost all time is spent in at lap 1.
for (i <- 1 to 3) {
val timeBefore = System.currentTimeMillis()
- renderPageImpl(engine.asInstanceOf[js.Invocable], WarmUpReactStoreJsonString)
+ renderPageImpl(engine, WarmUpReactStoreJsonString)
def timeElapsed = System.currentTimeMillis() - timeBefore
logger.info(o"""Warming up Nashorn engine, lap $i done, took: $timeElapsed ms""")
}
diff --git a/app/debiki/onebox/Onebox.scala b/app/debiki/onebox/Onebox.scala
index 77d200d08..355aaf48e 100644
--- a/app/debiki/onebox/Onebox.scala
+++ b/app/debiki/onebox/Onebox.scala
@@ -21,7 +21,7 @@ import com.debiki.core._
import com.debiki.core.Prelude._
import debiki.{Globals, Nashorn}
import debiki.onebox.engines._
-import javax.{script => js}
+import org.graalvm.{polyglot => graalvm}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.{ExecutionContext, Future}
@@ -78,7 +78,7 @@ abstract class OneboxEngine(globals: Globals, val nashorn: Nashorn) {
uploadsLinkRegex.replaceAllIn(safeHtml, s"""="$prefix$$1"""")
}
- final def loadRenderSanitize(url: String, javascriptEngine: Option[js.Invocable])
+ final def loadRenderSanitize(url: String, javascriptEngine: Option[graalvm.Context])
: Future[String] = {
def sanitizeAndWrap(html: String): String = {
var safeHtml =
@@ -157,7 +157,7 @@ class Onebox(val globals: Globals, val nashorn: Nashorn) {
new GiphyOnebox(globals, nashorn),
new YouTubeOnebox(globals, nashorn))
- def loadRenderSanitize(url: String, javascriptEngine: Option[js.Invocable])
+ def loadRenderSanitize(url: String, javascriptEngine: Option[graalvm.Context])
: Future[String] = {
for (engine <- engines) {
if (engine.handles(url))
@@ -167,7 +167,7 @@ class Onebox(val globals: Globals, val nashorn: Nashorn) {
}
- def loadRenderSanitizeInstantly(url: String, javascriptEngine: Option[js.Invocable])
+ def loadRenderSanitizeInstantly(url: String, javascriptEngine: Option[graalvm.Context])
: RenderOnboxResult = {
def placeholder = PlaceholderPrefix + nextRandomString()
@@ -203,7 +203,7 @@ class InstantOneboxRendererForNashorn(val oneboxes: Onebox) {
// Should be set to the Nashorn engine that calls this class, so that we can call
// back out to the same engine, when sanitizing html, so we won't have to ask for
// another engine, that'd create unnecessarily many engines.
- var javascriptEngine: Option[js.Invocable] = None
+ var javascriptEngine: Option[graalvm.Context] = None
def renderAndSanitizeOnebox(unsafeUrl: String): String = {
lazy val safeUrl = org.owasp.encoder.Encode.forHtml(unsafeUrl)
diff --git a/build.sbt b/build.sbt
index b89afa57f..edd2f1840 100644
--- a/build.sbt
+++ b/build.sbt
@@ -97,7 +97,9 @@ val appDependencies = Seq(
// CLEAN_UP remove Spec2 use only ScalaTest, need to edit some tests.
"org.mockito" % "mockito-all" % "1.9.0" % "test", // I use Mockito with Specs2...
"org.scalatest" %% "scalatest" % "3.0.5" % "test", // but prefer ScalaTest
- "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test)
+ "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test,
+ // GraalVM SDK for GraalVM JavaScript integration
+ "org.graalvm.sdk" % "graal-sdk" % "19.0.0")
val main = (project in file("."))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment