mirror of
https://github.com/fluencelabs/frank
synced 2025-04-24 14:02:13 +00:00
decouple frank from the main repo
This commit is contained in:
commit
3ef2f7f658
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Scala compiled
|
||||
target/
|
||||
|
||||
# Python compiled
|
||||
*.pyc
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
|
||||
# MacOS folder metadata
|
||||
.DS_Store
|
||||
|
||||
# Text writing
|
||||
*.scriv
|
||||
|
||||
# Temp
|
||||
docs/checklist.md
|
||||
|
||||
# Node modules
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Fluence CLI binary
|
||||
tools/deploy/fluence
|
||||
|
||||
# Contract typings and ABI
|
||||
Network.d.ts
|
||||
Network.json
|
||||
Dashboard.d.ts
|
56
.scalafmt.conf
Normal file
56
.scalafmt.conf
Normal file
@ -0,0 +1,56 @@
|
||||
version = 2.0.1
|
||||
|
||||
docstrings = JavaDoc
|
||||
|
||||
maxColumn = 120
|
||||
|
||||
align = some
|
||||
align.tokens = [{code = "=>", owner = "Case"}, ":=", "%", "%%", "%%%"]
|
||||
|
||||
assumeStandardLibraryStripMargin = true
|
||||
includeCurlyBraceInSelectChains = false
|
||||
|
||||
continuationIndent {
|
||||
callSite = 2
|
||||
defnSite = 2
|
||||
extendSite = 4
|
||||
}
|
||||
|
||||
danglingParentheses = true
|
||||
|
||||
newlines {
|
||||
alwaysBeforeTopLevelStatements = true
|
||||
sometimesBeforeColonInMethodReturnType = true
|
||||
penalizeSingleSelectMultiArgList = false
|
||||
alwaysBeforeElseAfterCurlyIf = false
|
||||
neverInResultType = false
|
||||
}
|
||||
|
||||
spaces {
|
||||
afterKeywordBeforeParen = true
|
||||
}
|
||||
|
||||
binPack {
|
||||
parentConstructors = true
|
||||
literalArgumentLists = true
|
||||
}
|
||||
|
||||
optIn {
|
||||
breaksInsideChains = false
|
||||
breakChainOnFirstMethodDot = true
|
||||
configStyleArguments = true
|
||||
}
|
||||
|
||||
runner {
|
||||
optimizer {
|
||||
forceConfigStyleOnOffset = 150
|
||||
forceConfigStyleMinArgCount = 2
|
||||
}
|
||||
}
|
||||
|
||||
rewrite {
|
||||
rules = [
|
||||
SortImports
|
||||
]
|
||||
}
|
||||
|
45
build.sbt
Normal file
45
build.sbt
Normal file
@ -0,0 +1,45 @@
|
||||
import SbtCommons._
|
||||
|
||||
name := "frank"
|
||||
|
||||
commons
|
||||
|
||||
/* Projects */
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.aggregate(`vm-scala`, `vm-rust`)
|
||||
|
||||
lazy val `vm-rust` = (project in file("vm/src/main/rust/"))
|
||||
.settings(
|
||||
compileFrankVMSettings()
|
||||
)
|
||||
|
||||
lazy val `vm-llamadb` = (project in file("vm/src/it/resources/llamadb"))
|
||||
.settings(
|
||||
downloadLlamadb()
|
||||
)
|
||||
|
||||
lazy val `vm-scala` = (project in file("vm"))
|
||||
.configs(IntegrationTest)
|
||||
.settings(inConfig(IntegrationTest)(Defaults.itSettings): _*)
|
||||
.settings(
|
||||
commons,
|
||||
libraryDependencies ++= Seq(
|
||||
cats,
|
||||
catsEffect,
|
||||
ficus,
|
||||
cryptoHashsign,
|
||||
scalaTest,
|
||||
scalaIntegrationTest,
|
||||
mockito
|
||||
),
|
||||
assemblyJarName in assembly := "frank.jar",
|
||||
assemblyMergeStrategy in assembly := SbtCommons.mergeStrategy.value,
|
||||
test in assembly := {},
|
||||
compile in Compile := (compile in Compile)
|
||||
.dependsOn(compile in `vm-rust`).value,
|
||||
test in IntegrationTest := (test in IntegrationTest)
|
||||
.dependsOn(compile in `vm-llamadb`)
|
||||
.value
|
||||
)
|
||||
.enablePlugins(AutomateHeaderPlugin)
|
207
project/SbtCommons.scala
Normal file
207
project/SbtCommons.scala
Normal file
@ -0,0 +1,207 @@
|
||||
import de.heikoseeberger.sbtheader.HeaderPlugin.autoImport.headerLicense
|
||||
import de.heikoseeberger.sbtheader.License
|
||||
import org.scalafmt.sbt.ScalafmtPlugin.autoImport.scalafmtOnCompile
|
||||
import sbt.Keys.{javaOptions, _}
|
||||
import sbt.{Def, addCompilerPlugin, taskKey, _}
|
||||
import sbtassembly.AssemblyPlugin.autoImport.assemblyMergeStrategy
|
||||
import sbtassembly.{MergeStrategy, PathList}
|
||||
|
||||
import scala.sys.process._
|
||||
|
||||
object SbtCommons {
|
||||
|
||||
val scalaV = scalaVersion := "2.12.9"
|
||||
|
||||
val kindProjector = Seq(
|
||||
resolvers += Resolver.sonatypeRepo("releases"),
|
||||
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.0")
|
||||
)
|
||||
|
||||
val commons = Seq(
|
||||
scalaV,
|
||||
version := "0.1.1",
|
||||
fork in Test := true,
|
||||
parallelExecution in Test := false,
|
||||
fork in IntegrationTest := true,
|
||||
parallelExecution in IntegrationTest := false,
|
||||
organizationName := "Fluence Labs Limited",
|
||||
organizationHomepage := Some(new URL("https://fluence.network")),
|
||||
startYear := Some(2019),
|
||||
licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt")),
|
||||
headerLicense := Some(License.ALv2("2019", organizationName.value)),
|
||||
resolvers += Resolver.bintrayRepo("fluencelabs", "releases"),
|
||||
scalafmtOnCompile := true,
|
||||
// see good explanation https://gist.github.com/djspiewak/7a81a395c461fd3a09a6941d4cd040f2
|
||||
scalacOptions ++= Seq("-Ypartial-unification", "-deprecation"),
|
||||
javaOptions in Test ++= Seq(
|
||||
"-XX:MaxMetaspaceSize=4G",
|
||||
"-Xms4G",
|
||||
"-Xmx4G",
|
||||
"-Xss6M",
|
||||
s"-Djava.library.path=${file("").getAbsolutePath}/vm/src/main/rust/target/release"
|
||||
),
|
||||
javaOptions in IntegrationTest ++= Seq(
|
||||
"-XX:MaxMetaspaceSize=4G",
|
||||
"-Xms4G",
|
||||
"-Xmx4G",
|
||||
"-Xss6M",
|
||||
s"-Djava.library.path=${file("").getAbsolutePath}/vm/src/main/rust/target/release"
|
||||
),
|
||||
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.0")
|
||||
) ++ kindProjector
|
||||
|
||||
val mergeStrategy = Def.setting[String => MergeStrategy]({
|
||||
// a module definition fails compilation for java 8, just skip it
|
||||
case PathList("module-info.class", xs @ _*) => MergeStrategy.first
|
||||
case "META-INF/io.netty.versions.properties" => MergeStrategy.first
|
||||
case x =>
|
||||
import sbtassembly.AssemblyPlugin.autoImport.assembly
|
||||
val oldStrategy = (assemblyMergeStrategy in assembly).value
|
||||
oldStrategy(x)
|
||||
}: String => MergeStrategy)
|
||||
|
||||
val rustToolchain = "nightly-2019-09-23"
|
||||
|
||||
def installPrerequisites() = {
|
||||
val installRust = s"curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $rustToolchain"
|
||||
val installToolchain = s"~/.cargo/bin/rustup toolchain install $rustToolchain"
|
||||
val installCross = "cargo install cross"
|
||||
|
||||
//assert((installRust !) == 0, "Rust installation failed")
|
||||
//assert((installToolchain !) == 0 , s"toolchain $rustToolchain installation failed")
|
||||
(installCross !)
|
||||
}
|
||||
|
||||
def compileFrank() = {
|
||||
val projectRoot = file("").getAbsolutePath
|
||||
val frankFolder = s"$projectRoot/vm/src/main/rust"
|
||||
val localCompileCmd = s"cargo +$rustToolchain build --manifest-path $frankFolder/Cargo.toml --release --lib"
|
||||
val crossCompileCmd = s"cd $frankFolder ; cross build --target x86_64-unknown-linux-gnu --release --lib"
|
||||
|
||||
println(crossCompileCmd)
|
||||
assert((localCompileCmd !) == 0, "Frank VM native compilation failed")
|
||||
assert((crossCompileCmd !) == 0, "Cross compilation to linux failed")
|
||||
}
|
||||
|
||||
def compileFrankVMSettings(): Seq[Def.Setting[_]] =
|
||||
Seq(
|
||||
publishArtifact := false,
|
||||
test := (test in Test).dependsOn(compile).value,
|
||||
compile := (compile in Compile)
|
||||
.dependsOn(Def.task {
|
||||
val log = streams.value.log
|
||||
|
||||
log.info("Installing prerequisites for native part compilation")
|
||||
installPrerequisites()
|
||||
|
||||
log.info("Compiling Frank VM")
|
||||
compileFrank()
|
||||
})
|
||||
.value
|
||||
)
|
||||
|
||||
def downloadLlamadb(): Seq[Def.Setting[_]] =
|
||||
Seq(
|
||||
publishArtifact := false,
|
||||
test := (test in Test).dependsOn(compile).value,
|
||||
compile := (compile in Compile)
|
||||
.dependsOn(Def.task {
|
||||
// by defaults, user.dir in sbt points to a submodule directory while in Idea to the project root
|
||||
val resourcesPath =
|
||||
if (System.getProperty("user.dir").endsWith("/vm"))
|
||||
System.getProperty("user.dir") + "/src/it/resources/"
|
||||
else
|
||||
System.getProperty("user.dir") + "/vm/src/it/resources/"
|
||||
|
||||
val log = streams.value.log
|
||||
val llamadbUrl = "https://github.com/fluencelabs/llamadb-wasm/releases/download/0.1.2/llama_db.wasm"
|
||||
val llamadbPreparedUrl =
|
||||
"https://github.com/fluencelabs/llamadb-wasm/releases/download/0.1.2/llama_db_prepared.wasm"
|
||||
|
||||
log.info(s"Dowloading llamadb from $llamadbUrl to $resourcesPath")
|
||||
|
||||
// -nc prevents downloading if file already exists
|
||||
val llamadbDownloadRet = s"wget -nc $llamadbUrl -O $resourcesPath/llama_db.wasm" !
|
||||
val llamadbPreparedDownloadRet = s"wget -nc $llamadbPreparedUrl -O $resourcesPath/llama_db_prepared.wasm" !
|
||||
|
||||
// wget returns 0 of file was downloaded and 1 if file already exists
|
||||
assert(llamadbDownloadRet == 0 || llamadbDownloadRet == 1, s"Download failed: $llamadbUrl")
|
||||
assert(
|
||||
llamadbPreparedDownloadRet == 0 || llamadbPreparedDownloadRet == 1,
|
||||
s"Download failed: $llamadbPreparedUrl"
|
||||
)
|
||||
})
|
||||
.value
|
||||
)
|
||||
|
||||
/* Common deps */
|
||||
|
||||
val catsVersion = "2.0.0"
|
||||
val cats = "org.typelevel" %% "cats-core" % catsVersion
|
||||
val catsEffectVersion = "2.0.0"
|
||||
val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion
|
||||
|
||||
val shapeless = "com.chuusai" %% "shapeless" % "2.3.3"
|
||||
|
||||
val fs2Version = "1.0.4"
|
||||
val fs2 = "co.fs2" %% "fs2-core" % fs2Version
|
||||
val fs2rx = "co.fs2" %% "fs2-reactive-streams" % fs2Version
|
||||
val fs2io = "co.fs2" %% "fs2-io" % fs2Version
|
||||
|
||||
// functional wrapper around 'lightbend/config'
|
||||
val ficus = "com.iheart" %% "ficus" % "1.4.5"
|
||||
|
||||
val cryptoVersion = "0.0.9"
|
||||
val cryptoHashsign = "one.fluence" %% "crypto-hashsign" % cryptoVersion
|
||||
val cryptoJwt = "one.fluence" %% "crypto-jwt" % cryptoVersion
|
||||
val cryptoCipher = "one.fluence" %% "crypto-cipher" % cryptoVersion
|
||||
|
||||
val codecVersion = "0.0.5"
|
||||
val codecCore = "one.fluence" %% "codec-core" % codecVersion
|
||||
|
||||
val sttpVersion = "1.6.3"
|
||||
val sttp = "com.softwaremill.sttp" %% "core" % sttpVersion
|
||||
val sttpCirce = "com.softwaremill.sttp" %% "circe" % sttpVersion
|
||||
val sttpFs2Backend = "com.softwaremill.sttp" %% "async-http-client-backend-fs2" % sttpVersion
|
||||
val sttpCatsBackend = "com.softwaremill.sttp" %% "async-http-client-backend-cats" % sttpVersion
|
||||
|
||||
val http4sVersion = "0.20.10"
|
||||
val http4sDsl = "org.http4s" %% "http4s-dsl" % http4sVersion
|
||||
val http4sServer = "org.http4s" %% "http4s-blaze-server" % http4sVersion
|
||||
val http4sCirce = "org.http4s" %% "http4s-circe" % http4sVersion
|
||||
|
||||
val circeVersion = "0.12.1"
|
||||
val circeCore = "io.circe" %% "circe-core" % circeVersion
|
||||
val circeGeneric = "io.circe" %% "circe-generic" % circeVersion
|
||||
val circeGenericExtras = "io.circe" %% "circe-generic-extras" % circeVersion
|
||||
val circeParser = "io.circe" %% "circe-parser" % circeVersion
|
||||
val circeFs2 = "io.circe" %% "circe-fs2" % "0.11.0"
|
||||
|
||||
val scodecBits = "org.scodec" %% "scodec-bits" % "1.1.9"
|
||||
val scodecCore = "org.scodec" %% "scodec-core" % "1.11.3"
|
||||
|
||||
val web3jVersion = "4.5.0"
|
||||
val web3jCrypto = "org.web3j" % "crypto" % web3jVersion
|
||||
val web3jCore = "org.web3j" % "core" % web3jVersion
|
||||
|
||||
val toml = "com.electronwill.night-config" % "toml" % "3.4.2"
|
||||
|
||||
val rocksDb = "org.rocksdb" % "rocksdbjni" % "5.17.2"
|
||||
val levelDb = "org.iq80.leveldb" % "leveldb" % "0.12"
|
||||
|
||||
val protobuf = "io.github.scalapb-json" %% "scalapb-circe" % "0.4.3"
|
||||
val protobufUtil = "com.google.protobuf" % "protobuf-java-util" % "3.7.1"
|
||||
|
||||
val bouncyCastle = "org.bouncycastle" % "bcprov-jdk15on" % "1.61"
|
||||
|
||||
val asyncHttpClient = "org.asynchttpclient" % "async-http-client" % "2.8.1"
|
||||
|
||||
/* Test deps*/
|
||||
val scalacheckShapeless = "com.github.alexarchambault" %% "scalacheck-shapeless_1.13" % "1.1.8" % Test
|
||||
val catsTestkit = "org.typelevel" %% "cats-testkit" % catsVersion % Test
|
||||
val disciplineScalaTest = "org.typelevel" %% "discipline-scalatest" % "1.0.0-M1" % Test
|
||||
|
||||
val scalaTest = "org.scalatest" %% "scalatest" % "3.0.8" % Test
|
||||
val scalaIntegrationTest = "org.scalatest" %% "scalatest" % "3.0.8" % IntegrationTest
|
||||
val mockito = "org.mockito" % "mockito-core" % "2.21.0" % Test
|
||||
}
|
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@ -0,0 +1 @@
|
||||
sbt.version=1.2.8
|
9
project/plugins.sbt
Normal file
9
project/plugins.sbt
Normal file
@ -0,0 +1,9 @@
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.1")
|
||||
|
||||
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0")
|
||||
|
||||
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
|
||||
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
|
||||
|
||||
addSbtPlugin("ch.jodersky" % "sbt-jni" % "1.3.4")
|
2
vm/src/it/resources/.gitignore
vendored
Normal file
2
vm/src/it/resources/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Downloaded test artifacts
|
||||
*.wasm
|
60
vm/src/it/resources/reference.conf
Normal file
60
vm/src/it/resources/reference.conf
Normal file
@ -0,0 +1,60 @@
|
||||
#
|
||||
# These settings describe VM configs for integrations test launches with different memory sizes.
|
||||
# More info about each field meaning can be found in vm/src/main/resources/reference.conf.
|
||||
#
|
||||
|
||||
fluence.vm.client.4Mb {
|
||||
|
||||
// 65536 * 64 = 4 Mb
|
||||
memPagesCount: 64
|
||||
|
||||
loggerEnabled: true
|
||||
|
||||
chunkSize: 4096
|
||||
|
||||
mainModuleConfig: {
|
||||
allocateFunctionName: "allocate"
|
||||
|
||||
deallocateFunctionName: "deallocate"
|
||||
|
||||
invokeFunctionName: "invoke"
|
||||
}
|
||||
}
|
||||
|
||||
fluence.vm.client.100Mb {
|
||||
|
||||
// 65536 * 1600 = 100 Mb
|
||||
memPagesCount: 1600
|
||||
|
||||
loggerEnabled: true
|
||||
|
||||
chunkSize: 4096
|
||||
|
||||
mainModuleConfig: {
|
||||
allocateFunctionName: "allocate"
|
||||
|
||||
deallocateFunctionName: "deallocate"
|
||||
|
||||
invokeFunctionName: "invoke"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fluence.vm.client.2Gb {
|
||||
|
||||
// 65536 * 32767 ~ 2 Gb
|
||||
memPagesCount: 12767
|
||||
|
||||
loggerEnabled: true
|
||||
|
||||
chunkSize: 4096
|
||||
|
||||
mainModuleConfig: {
|
||||
allocateFunctionName: "allocate"
|
||||
|
||||
deallocateFunctionName: "deallocate"
|
||||
|
||||
invokeFunctionName: "invoke"
|
||||
}
|
||||
|
||||
}
|
45
vm/src/it/scala/fluence/vm/AppIntegrationTest.scala
Normal file
45
vm/src/it/scala/fluence/vm/AppIntegrationTest.scala
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2018 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.IO
|
||||
import org.scalatest.{Assertion, EitherValues, Matchers, OptionValues, WordSpec}
|
||||
|
||||
trait AppIntegrationTest extends WordSpec with Matchers with OptionValues with EitherValues {
|
||||
|
||||
protected def checkTestResult(result: InvocationResult, expectedString: String): Assertion = {
|
||||
val resultAsString = new String(result.output)
|
||||
resultAsString should startWith(expectedString)
|
||||
}
|
||||
|
||||
protected def compareArrays(first: Array[Byte], second: Array[Byte]): Assertion =
|
||||
first.deep shouldBe second.deep
|
||||
|
||||
implicit class EitherTValueReader[E <: Throwable, V](origin: EitherT[IO, E, V]) {
|
||||
|
||||
def success(): V =
|
||||
origin.value.unsafeRunSync() match {
|
||||
case Left(e) => println(s"got error $e"); throw e
|
||||
case Right(v) => v
|
||||
}
|
||||
|
||||
def failed(): E =
|
||||
origin.value.unsafeRunSync().left.value
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2018 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.{IO, Timer}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.language.{higherKinds, implicitConversions}
|
||||
import fluence.vm.Utils.getModuleDirPrefix
|
||||
|
||||
// TODO: to run this test from IDE It needs to download vm-llamadb project explicitly at first
|
||||
// this test is separated from the main LlamadbIntegrationTest because gas price for each instruction could be changed
|
||||
// and it would be difficult to update them in each test
|
||||
class LlamadbInstrumentedIntegrationTest extends LlamadbIntegrationTestInterface {
|
||||
|
||||
override val llamadbFilePath: String = getModuleDirPrefix() +
|
||||
"/src/it/resources/llama_db_prepared.wasm"
|
||||
|
||||
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
|
||||
|
||||
"instrumented llamadb app" should {
|
||||
|
||||
"be able to instantiate" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
state ← vm.computeVmState[IO].toVmError
|
||||
} yield {
|
||||
state should not be None
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to create table and insert to it" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
createResult ← createTestTable(vm)
|
||||
|
||||
} yield {
|
||||
checkTestResult(createResult, "rows inserted")
|
||||
createResult.spentGas should equal(527599L)
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to select records" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
createResult ← createTestTable(vm)
|
||||
emptySelectResult ← executeSql(vm, "SELECT * FROM Users WHERE name = 'unknown'")
|
||||
selectAllResult ← executeSql(vm, "SELECT min(id), max(id), count(age), sum(age), avg(age) FROM Users")
|
||||
explainResult ← executeSql(vm, "EXPLAIN SELECT id, name FROM Users")
|
||||
|
||||
} yield {
|
||||
checkTestResult(createResult, "rows inserted")
|
||||
checkTestResult(emptySelectResult, "id, name, age")
|
||||
checkTestResult(
|
||||
selectAllResult,
|
||||
"_0, _1, _2, _3, _4\n" +
|
||||
"1, 4, 4, 98, 24.5"
|
||||
)
|
||||
checkTestResult(
|
||||
explainResult,
|
||||
"query plan\n" +
|
||||
"column names: (`id`, `name`)\n" +
|
||||
"(scan `users` :source-id 0\n" +
|
||||
" (yield\n" +
|
||||
" (column-field :source-id 0 :column-offset 0)\n" +
|
||||
" (column-field :source-id 0 :column-offset 1)))"
|
||||
)
|
||||
|
||||
createResult.spentGas should equal(527599L)
|
||||
emptySelectResult.spentGas should equal(370143L)
|
||||
selectAllResult.spentGas should equal(754557L)
|
||||
explainResult.spentGas should equal(387359L)
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to launch VM with 2 GiB memory and a lot of data inserts" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.2Gb")
|
||||
_ ← createTestTable(vm)
|
||||
|
||||
// trying to insert 30 times by ~200 KiB
|
||||
_ = for (_ ← 1 to 30) yield { executeInsert(vm, 200) }.value.unsafeRunSync
|
||||
insertResult ← executeInsert(vm, 1)
|
||||
|
||||
} yield {
|
||||
checkTestResult(insertResult, "rows inserted")
|
||||
insertResult.spentGas should equal(1469167L)
|
||||
|
||||
}).success()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
285
vm/src/it/scala/fluence/vm/LlamadbIntegrationTest.scala
Normal file
285
vm/src/it/scala/fluence/vm/LlamadbIntegrationTest.scala
Normal file
@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2018 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.{IO, Timer}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.language.{higherKinds, implicitConversions}
|
||||
|
||||
// TODO: to run this test from IDE It needs to download vm-llamadb project explicitly at first
|
||||
class LlamadbIntegrationTest extends LlamadbIntegrationTestInterface {
|
||||
|
||||
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
|
||||
|
||||
"llamadb app" should {
|
||||
|
||||
"be able to instantiate" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
state ← vm.computeVmState[IO].toVmError
|
||||
} yield {
|
||||
state should not be None
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to create table and insert to it" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
createResult ← createTestTable(vm)
|
||||
|
||||
} yield {
|
||||
checkTestResult(createResult, "rows inserted")
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to select records" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
createResult ← createTestTable(vm)
|
||||
emptySelectResult ← executeSql(vm, "SELECT * FROM Users WHERE name = 'unknown'")
|
||||
selectAllResult ← executeSql(vm, "SELECT min(id), max(id), count(age), sum(age), avg(age) FROM Users")
|
||||
explainResult ← executeSql(vm, "EXPLAIN SELECT id, name FROM Users")
|
||||
|
||||
} yield {
|
||||
checkTestResult(createResult, "rows inserted")
|
||||
checkTestResult(emptySelectResult, "id, name, age")
|
||||
checkTestResult(
|
||||
selectAllResult,
|
||||
"_0, _1, _2, _3, _4\n" +
|
||||
"1, 4, 4, 98, 24.5"
|
||||
)
|
||||
checkTestResult(
|
||||
explainResult,
|
||||
"query plan\n" +
|
||||
"column names: (`id`, `name`)\n" +
|
||||
"(scan `users` :source-id 0\n" +
|
||||
" (yield\n" +
|
||||
" (column-field :source-id 0 :column-offset 0)\n" +
|
||||
" (column-field :source-id 0 :column-offset 1)))"
|
||||
)
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to delete records and drop table" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
createResult1 ← createTestTable(vm)
|
||||
deleteResult ← executeSql(vm, "DELETE FROM Users WHERE id = 1")
|
||||
selectAfterDeleteTable ← executeSql(vm, "SELECT * FROM Users WHERE id = 1")
|
||||
|
||||
truncateResult ← executeSql(vm, "TRUNCATE TABLE Users")
|
||||
selectFromTruncatedTableResult ← executeSql(vm, "SELECT * FROM Users")
|
||||
|
||||
createResult2 ← createTestTable(vm)
|
||||
dropTableResult ← executeSql(vm, "DROP TABLE Users")
|
||||
selectFromDroppedTableResult ← executeSql(vm, "SELECT * FROM Users")
|
||||
|
||||
} yield {
|
||||
checkTestResult(createResult1, "rows inserted")
|
||||
checkTestResult(deleteResult, "rows deleted: 1")
|
||||
checkTestResult(selectAfterDeleteTable, "id, name, age")
|
||||
checkTestResult(truncateResult, "rows deleted: 3")
|
||||
checkTestResult(selectFromTruncatedTableResult, "id, name, age")
|
||||
checkTestResult(createResult2, "rows inserted")
|
||||
checkTestResult(dropTableResult, "table was dropped")
|
||||
checkTestResult(selectFromDroppedTableResult, "[Error] table does not exist: users")
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to manipulate with 2 tables and selects records with join" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
createResult ← createTestTable(vm)
|
||||
createRoleResult ← executeSql(vm, "CREATE TABLE Roles(user_id INT, role VARCHAR(128))")
|
||||
roleInsertResult ← executeSql(
|
||||
vm,
|
||||
"INSERT INTO Roles VALUES(1, 'Teacher'), (2, 'Student'), (3, 'Scientist'), (4, 'Writer')"
|
||||
)
|
||||
selectWithJoinResult ← executeSql(
|
||||
vm,
|
||||
"SELECT u.name AS Name, r.role AS Role FROM Users u JOIN Roles r ON u.id = r.user_id WHERE r.role = 'Writer'"
|
||||
)
|
||||
deleteResult ← executeSql(
|
||||
vm,
|
||||
"DELETE FROM Users WHERE id = (SELECT user_id FROM Roles WHERE role = 'Student')"
|
||||
)
|
||||
updateResult ← executeSql(
|
||||
vm,
|
||||
"UPDATE Roles r SET r.role = 'Professor' WHERE r.user_id = " +
|
||||
"(SELECT id FROM Users WHERE name = 'Sara')"
|
||||
)
|
||||
|
||||
} yield {
|
||||
checkTestResult(createResult, "rows inserted")
|
||||
checkTestResult(createRoleResult, "table created")
|
||||
checkTestResult(roleInsertResult, "rows inserted: 4")
|
||||
checkTestResult(
|
||||
selectWithJoinResult,
|
||||
"name, role\n" +
|
||||
"Tagless Final, Writer"
|
||||
)
|
||||
checkTestResult(deleteResult, "rows deleted: 1")
|
||||
checkTestResult(updateResult, "[Error] subquery must yield exactly one row")
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to operate with empty strings" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
_ ← executeSql(vm, "")
|
||||
_ ← createTestTable(vm)
|
||||
emptyQueryResult ← executeSql(vm, "")
|
||||
|
||||
} yield {
|
||||
checkTestResult(
|
||||
emptyQueryResult,
|
||||
"[Error] Expected SELECT, INSERT, CREATE, DELETE, TRUNCATE or EXPLAIN statement; got no more tokens"
|
||||
)
|
||||
|
||||
}).success()
|
||||
}
|
||||
|
||||
"doesn't fail with incorrect queries" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
_ ← createTestTable(vm)
|
||||
invalidQueryResult ← executeSql(vm, "SELECT salary FROM Users")
|
||||
parserErrorResult ← executeSql(vm, "123")
|
||||
incompatibleTypeResult ← executeSql(vm, "SELECT * FROM Users WHERE age = 'Bob'")
|
||||
|
||||
} yield {
|
||||
checkTestResult(invalidQueryResult, "[Error] column does not exist: salary")
|
||||
checkTestResult(
|
||||
parserErrorResult,
|
||||
"[Error] Expected SELECT, INSERT, CREATE, DELETE, TRUNCATE or EXPLAIN statement; got Number(\"123\")"
|
||||
)
|
||||
checkTestResult(incompatibleTypeResult, "[Error] 'Bob' cannot be cast to Integer { signed: true, bytes: 8 }")
|
||||
|
||||
}).success()
|
||||
}
|
||||
|
||||
"be able to launch VM with 4 MiB memory and to insert a lot of data" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
_ ← createTestTable(vm)
|
||||
|
||||
// allocate ~1 MiB memory
|
||||
insertResult1 ← executeInsert(vm, 512)
|
||||
insertResult2 ← executeInsert(vm, 512)
|
||||
|
||||
} yield {
|
||||
checkTestResult(insertResult1, "rows inserted")
|
||||
checkTestResult(insertResult2, "rows inserted")
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to launch VM with 4 MiB memory and a lot of data inserts" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
|
||||
_ ← createTestTable(vm)
|
||||
|
||||
// trying to insert 1024 time by 1 KiB
|
||||
_ = for (_ ← 1 to 1024) yield { executeInsert(vm, 1) }.value.unsafeRunSync
|
||||
insertResult ← executeInsert(vm, 1)
|
||||
|
||||
} yield {
|
||||
checkTestResult(insertResult, "rows inserted")
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to launch VM with 100 MiB memory and to insert a lot of data" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.100Mb")
|
||||
_ ← createTestTable(vm)
|
||||
|
||||
// allocate ~30 MiB memory
|
||||
insertResult1 ← executeInsert(vm, 15 * 1024)
|
||||
insertResult2 ← executeInsert(vm, 15 * 1024)
|
||||
|
||||
} yield {
|
||||
checkTestResult(insertResult1, "rows inserted")
|
||||
checkTestResult(insertResult2, "rows inserted")
|
||||
|
||||
}).success()
|
||||
}
|
||||
|
||||
"be able to launch VM with 100 MiB memory and a lot of data inserts" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.100Mb")
|
||||
_ ← createTestTable(vm)
|
||||
|
||||
// trying to insert 30 time by ~100 KiB
|
||||
_ = for (_ ← 1 to 30) yield { executeInsert(vm, 100) }.value.unsafeRunSync
|
||||
insertResult ← executeInsert(vm, 1)
|
||||
|
||||
} yield {
|
||||
checkTestResult(insertResult, "rows inserted")
|
||||
|
||||
}).success()
|
||||
|
||||
}
|
||||
|
||||
"be able to launch VM with 2 GiB memory and to allocate 256 MiB of continuously memory" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.2Gb")
|
||||
_ ← executeSql(vm, "create table USERS(name varchar(" + 256 * 1024 * 1024 + "))")
|
||||
|
||||
// trying to insert two records to ~256 MiB field
|
||||
insertResult1 ← executeSql(vm, "insert into USERS values(\'" + "A" * 1024 + "\')")
|
||||
insertResult2 ← executeSql(vm, "insert into USERS values(\'" + "A" * 1024 + "\')")
|
||||
|
||||
} yield {
|
||||
checkTestResult(insertResult1, "rows inserted")
|
||||
checkTestResult(insertResult2, "rows inserted")
|
||||
|
||||
}).success()
|
||||
}
|
||||
|
||||
"be able to launch VM with 2 GiB memory and a lot of data inserts" in {
|
||||
(for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.2Gb")
|
||||
_ ← createTestTable(vm)
|
||||
|
||||
// trying to insert 30 time by ~200 KiB
|
||||
_ = for (_ ← 1 to 30) yield { executeInsert(vm, 200) }.value.unsafeRunSync
|
||||
insertResult ← executeInsert(vm, 1)
|
||||
|
||||
} yield {
|
||||
checkTestResult(insertResult, "rows inserted")
|
||||
|
||||
}).success()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2018 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.IO
|
||||
import fluence.vm.error.VmError
|
||||
import org.scalatest.EitherValues
|
||||
import fluence.vm.Utils.getModuleDirPrefix
|
||||
|
||||
import scala.language.{higherKinds, implicitConversions}
|
||||
|
||||
trait LlamadbIntegrationTestInterface extends AppIntegrationTest with EitherValues {
|
||||
|
||||
protected val llamadbFilePath: String = getModuleDirPrefix() +
|
||||
"/src/it/resources/llama_db.wasm"
|
||||
|
||||
protected def executeSql(implicit vm: WasmVm, sql: String): EitherT[IO, VmError, InvocationResult] =
|
||||
for {
|
||||
result ← vm.invoke[IO](sql.getBytes())
|
||||
_ ← vm.computeVmState[IO].toVmError
|
||||
} yield result
|
||||
|
||||
protected def createTestTable(vm: WasmVm): EitherT[IO, VmError, InvocationResult] =
|
||||
for {
|
||||
_ ← executeSql(vm, "CREATE TABLE Users(id INT, name TEXT, age INT)")
|
||||
insertResult ← executeSql(
|
||||
vm,
|
||||
"INSERT INTO Users VALUES(1, 'Monad', 23)," +
|
||||
"(2, 'Applicative Functor', 19)," +
|
||||
"(3, 'Free Monad', 31)," +
|
||||
"(4, 'Tagless Final', 25)"
|
||||
)
|
||||
} yield insertResult
|
||||
|
||||
// inserts about (recordsCount KiB + const bytes)
|
||||
protected def executeInsert(vm: WasmVm, recordsCount: Int): EitherT[IO, VmError, InvocationResult] =
|
||||
for {
|
||||
result ← executeSql(
|
||||
vm,
|
||||
"INSERT into USERS VALUES(1, 'A', 1)" + (",(1, \'" + "A" * 1024 + "\', 1)") * recordsCount
|
||||
)
|
||||
} yield result
|
||||
|
||||
}
|
34
vm/src/main/resources/reference.conf
Normal file
34
vm/src/main/resources/reference.conf
Normal file
@ -0,0 +1,34 @@
|
||||
#
|
||||
# These settings describe the reasonable defaults for WasmVm.
|
||||
#
|
||||
|
||||
fluence.vm.client {
|
||||
|
||||
# To obtain deterministic execution, all Wasm memory is preallocated on the VM startup.
|
||||
# This parameter defines count of Wasm pages that should be preallocated. Each page contains 65536 bytes of data,
|
||||
# `65536 * 1600 ~ 100MB`
|
||||
memPagesCount: 1600
|
||||
|
||||
# if true, allows Wasm code to use logging
|
||||
loggerEnabled: true
|
||||
|
||||
# Memory will be split by chunks to be able to build Merkle Tree on top of it.
|
||||
# Size of memory in bytes must be dividable by chunkSize.
|
||||
chunkSize: 4096
|
||||
|
||||
mainModuleConfig: {
|
||||
# The main module name according to the conventions should be non set
|
||||
# name: "main"
|
||||
|
||||
# The name of function that should be called for allocation memory. This function
|
||||
# is used for passing array of bytes to the main module.
|
||||
allocateFunctionName: "allocate"
|
||||
|
||||
# The name of function that should be called for deallocation of
|
||||
# previously allocated memory by allocateFunction.
|
||||
deallocateFunctionName: "deallocate"
|
||||
|
||||
# The name of the main module handler function.
|
||||
invokeFunctionName: "invoke"
|
||||
}
|
||||
}
|
1272
vm/src/main/rust/Cargo.lock
generated
Normal file
1272
vm/src/main/rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
vm/src/main/rust/Cargo.toml
Normal file
41
vm/src/main/rust/Cargo.toml
Normal file
@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "Frank"
|
||||
description = "Virtual machine based on Wasmer for the Fluence network"
|
||||
version = "0.1.0"
|
||||
authors = ["Fluence Labs"]
|
||||
edition = "2018"
|
||||
license = "Apache-2.0"
|
||||
keywords = ["fluence", "webassembly", "wasmer", "execution environment"]
|
||||
categories = ["webassembly"]
|
||||
repository = "https://github.com/fluencelabs/fluence/tree/master/vm/frank"
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[lib]
|
||||
name = "frank"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[[bin]]
|
||||
name = "frank"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
wasmer-runtime = {git = "http://github.com/fluencelabs/wasmer", branch = "clif_jni_hardering"}
|
||||
wasmer-runtime-core = {git = "http://github.com/fluencelabs/wasmer", branch = "clif_jni_hardering"}
|
||||
jni = "0.13.1"
|
||||
failure = "0.1.5"
|
||||
lazy_static = "1.4.0"
|
||||
sha2 = "0.8.0"
|
||||
clap = "2.33.0"
|
||||
exitfailure = "0.5.1"
|
||||
boolinator = "2.4.0"
|
||||
parity-wasm = "0.40.3"
|
||||
pwasm-utils = "0.11.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
lto = true
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
panic = "abort"
|
143
vm/src/main/rust/src/jni/exports.rs
Normal file
143
vm/src/main/rust/src/jni/exports.rs
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// Defines export functions that will be accessible from the Scala part.
|
||||
|
||||
use crate::jni::jni_results::*;
|
||||
use crate::vm::config::Config;
|
||||
use crate::vm::errors::FrankError;
|
||||
use crate::vm::frank::{Frank, FRANK};
|
||||
use crate::vm::frank_result::FrankResult;
|
||||
use crate::vm::prepare::prepare_module;
|
||||
|
||||
use jni::objects::{JClass, JObject, JString};
|
||||
use jni::sys::jbyteArray;
|
||||
use jni::JNIEnv;
|
||||
use sha2::digest::generic_array::GenericArray;
|
||||
use std::fs;
|
||||
|
||||
/// Initializes Frank virtual machine.
|
||||
/// This method is exported to Scala.
|
||||
///
|
||||
/// Arguments
|
||||
///
|
||||
/// * env - corresponding java native interface
|
||||
/// * _class - represents caller class
|
||||
/// * module_path - a path to module that should be loaded to Frank virtual machine
|
||||
/// * config - contains some configs that manage module loading and instantiation process
|
||||
///
|
||||
/// Returns a object of RawInitializationResult case class
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_fluence_vm_frank_FrankAdapter_initialize<'a>(
|
||||
env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
module_path: JString,
|
||||
config: JObject,
|
||||
) -> JObject<'a> {
|
||||
fn initialize<'a>(
|
||||
env: &JNIEnv<'a>,
|
||||
module_path: JString,
|
||||
config: JObject,
|
||||
) -> Result<(bool), FrankError> {
|
||||
let module_path: String = env.get_string(module_path)?.into();
|
||||
let config = Config::new(&env, config)?;
|
||||
|
||||
let wasm_code = fs::read(module_path)?;
|
||||
let prepared_module = prepare_module(&wasm_code, &config)?;
|
||||
let frank = Frank::new(&prepared_module, config)?;
|
||||
|
||||
unsafe { FRANK = Some(Box::new(frank.0)) };
|
||||
|
||||
Ok(frank.1)
|
||||
}
|
||||
|
||||
match initialize(&env, module_path, config) {
|
||||
Ok(expects_eths) => create_initialization_result(&env, None, expects_eths),
|
||||
Err(err) => create_initialization_result(&env, Some(format!("{}", err)), false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Invokes the main module entry point function.
|
||||
/// This method is exported to Scala.
|
||||
///
|
||||
/// Arguments
|
||||
///
|
||||
/// * env - corresponding java native interface
|
||||
/// * _class - represents caller class
|
||||
/// * fn_argument - an argument for thr main module entry point function
|
||||
///
|
||||
/// Returns a object of RawInvocationResult case class
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_fluence_vm_frank_FrankAdapter_invoke<'a>(
|
||||
env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
fn_argument: jbyteArray,
|
||||
) -> JObject<'a> {
|
||||
fn invoke(env: &JNIEnv, fn_argument: jbyteArray) -> Result<FrankResult, FrankError> {
|
||||
let input_len = env.get_array_length(fn_argument)?;
|
||||
let mut input = vec![0; input_len as _];
|
||||
env.get_byte_array_region(fn_argument, 0, input.as_mut_slice())?;
|
||||
|
||||
// converts Vec<i8> to Vec<u8> without additional allocation
|
||||
let u8_input = unsafe {
|
||||
Vec::<u8>::from_raw_parts(input.as_mut_ptr() as *mut u8, input.len(), input.capacity())
|
||||
};
|
||||
std::mem::forget(input);
|
||||
|
||||
unsafe {
|
||||
match FRANK {
|
||||
Some(ref mut vm) => Ok(vm.invoke(&u8_input)?),
|
||||
None => Err(FrankError::FrankNotInitialized),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match invoke(&env, fn_argument) {
|
||||
Ok(result) => create_invocation_result(&env, None, result),
|
||||
Err(err) => {
|
||||
create_invocation_result(&env, Some(format!("{}", err)), FrankResult::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes hash of the internal VM state.
|
||||
/// This method is exported to Scala.
|
||||
///
|
||||
/// Arguments
|
||||
///
|
||||
/// * env - corresponding java native interface
|
||||
/// * _class - represents caller class
|
||||
///
|
||||
/// Returns a object of RawStateComputationResult case class
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_fluence_vm_frank_FrankAdapter_computeVmState<'a>(
|
||||
env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
) -> JObject<'a> {
|
||||
unsafe {
|
||||
match FRANK {
|
||||
Some(ref mut vm) => {
|
||||
let state = vm.compute_vm_state_hash();
|
||||
create_state_computation_result(&env, None, state)
|
||||
}
|
||||
None => create_state_computation_result(
|
||||
&env,
|
||||
Some(format!("{}", FrankError::FrankNotInitialized)),
|
||||
GenericArray::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
109
vm/src/main/rust/src/jni/jni_results.rs
Normal file
109
vm/src/main/rust/src/jni/jni_results.rs
Normal file
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// Defines functions used to construct result of the VM invocation for the Scala part.
|
||||
/// Corresponding case classes could be found in vm/src/main/scala/fluence/vm/frank/result.
|
||||
use crate::jni::option::*;
|
||||
use crate::vm::frank_result::FrankResult;
|
||||
use jni::objects::{JObject, JValue};
|
||||
use jni::JNIEnv;
|
||||
use sha2::{digest::generic_array::GenericArray, digest::FixedOutput, Sha256};
|
||||
|
||||
/// Creates RawInitializationResult object for the Scala part.
|
||||
pub fn create_initialization_result<'a>(
|
||||
env: &JNIEnv<'a>,
|
||||
error: Option<String>,
|
||||
expects_eths: bool,
|
||||
) -> JObject<'a> {
|
||||
let error_value = match error {
|
||||
Some(err) => create_some_value(&env, err),
|
||||
None => create_none_value(&env),
|
||||
};
|
||||
|
||||
// public static RawInitializationResult apply(final Option error, final Boolean expects_eth) {
|
||||
// return RawInitializationResult$.MODULE$.apply(var0);
|
||||
// }
|
||||
env.call_static_method(
|
||||
"fluence/vm/frank/result/RawInitializationResult",
|
||||
"apply",
|
||||
"(Lscala/Option;Z)Lfluence/vm/frank/result/RawInitializationResult;",
|
||||
&[error_value, JValue::from(expects_eths)],
|
||||
)
|
||||
.expect("jni: couldn't allocate a new RawInitializationResult object")
|
||||
.l()
|
||||
.expect("jni: couldn't convert RawInitializationResult to a Java Object")
|
||||
}
|
||||
|
||||
/// Creates RawInvocationResult object for the Scala part.
|
||||
pub fn create_invocation_result<'a>(
|
||||
env: &JNIEnv<'a>,
|
||||
error: Option<String>,
|
||||
result: FrankResult,
|
||||
) -> JObject<'a> {
|
||||
let error_value = match error {
|
||||
Some(err) => create_some_value(&env, err),
|
||||
None => create_none_value(&env),
|
||||
};
|
||||
|
||||
// TODO: here we have 2 copying of result, first is from Wasm memory to a Vec<u8>, second is
|
||||
// from the Vec<u8> to Java byte array. Optimization might be possible after memory refactoring.
|
||||
let outcome = env.byte_array_from_slice(&result.outcome).unwrap();
|
||||
let outcome = JObject::from(outcome);
|
||||
let spent_gas = JValue::from(result.spent_gas);
|
||||
|
||||
// public static RawInvocationResult apply(final Option error, final byte[] output, final long spentGas) {
|
||||
// return RawInvocationResult$.MODULE$.apply(var0, var1, var2);
|
||||
// }
|
||||
env.call_static_method(
|
||||
"fluence/vm/frank/result/RawInvocationResult",
|
||||
"apply",
|
||||
"(Lscala/Option;[BJ)Lfluence/vm/frank/result/RawInvocationResult;",
|
||||
&[error_value, JValue::from(outcome), spent_gas],
|
||||
)
|
||||
.expect("jni: couldn't allocate a new RawInvocationResult object")
|
||||
.l()
|
||||
.expect("jni: couldn't convert RawInvocationResult to a Java Object")
|
||||
}
|
||||
|
||||
/// Creates RawStateComputationResult object for the Scala part.
|
||||
pub fn create_state_computation_result<'a>(
|
||||
env: &JNIEnv<'a>,
|
||||
error: Option<String>,
|
||||
state: GenericArray<u8, <Sha256 as FixedOutput>::OutputSize>,
|
||||
) -> JObject<'a> {
|
||||
let error_value = match error {
|
||||
Some(err) => create_some_value(&env, err),
|
||||
None => create_none_value(&env),
|
||||
};
|
||||
|
||||
let state = env
|
||||
.byte_array_from_slice(state.as_slice())
|
||||
.expect("jni: couldn't allocate enough space for byte array");
|
||||
let state = JObject::from(state);
|
||||
|
||||
// public static RawStateComputationResult apply(final Option error, final byte[] state) {
|
||||
// return RawStateComputationResult$.MODULE$.apply(var0, var1);
|
||||
// }
|
||||
env.call_static_method(
|
||||
"fluence/vm/frank/result/RawStateComputationResult",
|
||||
"apply",
|
||||
"(Lscala/Option;[B)Lfluence/vm/frank/result/RawStateComputationResult;",
|
||||
&[error_value, JValue::from(state)],
|
||||
)
|
||||
.expect("jni: couldn't allocate a new RawInvocationResult object")
|
||||
.l()
|
||||
.expect("jni: couldn't convert RawInvocationResult to a Java Object")
|
||||
}
|
19
vm/src/main/rust/src/jni/mod.rs
Normal file
19
vm/src/main/rust/src/jni/mod.rs
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
pub mod exports;
|
||||
mod jni_results;
|
||||
mod option;
|
52
vm/src/main/rust/src/jni/option.rs
Normal file
52
vm/src/main/rust/src/jni/option.rs
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
use jni::objects::{JObject, JValue};
|
||||
/// Defines functions to construct the Scala Option[String] objects by calling the apply method:
|
||||
///
|
||||
/// ```
|
||||
/// public static Option apply(final Object x) {
|
||||
/// return Option$.MODULE$.apply(var0);
|
||||
/// }
|
||||
/// ```
|
||||
use jni::JNIEnv;
|
||||
|
||||
/// creates Scala None value
|
||||
pub fn create_none_value<'a>(env: &JNIEnv<'a>) -> JValue<'a> {
|
||||
env.call_static_method(
|
||||
"scala/Option",
|
||||
"apply",
|
||||
"(Ljava/lang/Object;)Lscala/Option;",
|
||||
&[JValue::from(JObject::null())],
|
||||
)
|
||||
.expect("jni: error while creating None object")
|
||||
}
|
||||
|
||||
/// creates Scala Some[String] value
|
||||
pub fn create_some_value<'a>(env: &JNIEnv<'a>, value: String) -> JValue<'a> {
|
||||
let value = env
|
||||
.new_string(value)
|
||||
.expect("jni: couldn't allocate new string");
|
||||
|
||||
let value = JObject::from(value);
|
||||
env.call_static_method(
|
||||
"scala/Option",
|
||||
"apply",
|
||||
"(Ljava/lang/Object;)Lscala/Option;",
|
||||
&[JValue::from(value)],
|
||||
)
|
||||
.expect("jni: couldn't allocate a Some object")
|
||||
}
|
28
vm/src/main/rust/src/lib.rs
Normal file
28
vm/src/main/rust/src/lib.rs
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#![deny(
|
||||
dead_code,
|
||||
nonstandard_style,
|
||||
unused_imports,
|
||||
unused_mut,
|
||||
unused_variables,
|
||||
unused_unsafe,
|
||||
unreachable_patterns
|
||||
)]
|
||||
|
||||
mod jni;
|
||||
mod vm;
|
107
vm/src/main/rust/src/main.rs
Normal file
107
vm/src/main/rust/src/main.rs
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#![deny(
|
||||
dead_code,
|
||||
nonstandard_style,
|
||||
unused_imports,
|
||||
unused_mut,
|
||||
unused_variables,
|
||||
unused_unsafe,
|
||||
unreachable_patterns
|
||||
)]
|
||||
|
||||
/// Command-line tool intended to test Frank VM.
|
||||
mod jni;
|
||||
mod vm;
|
||||
|
||||
use crate::vm::config::Config;
|
||||
use crate::vm::prepare::prepare_module;
|
||||
use crate::vm::frank::Frank;
|
||||
|
||||
use clap::{App, AppSettings, Arg, SubCommand};
|
||||
use exitfailure::ExitFailure;
|
||||
use failure::err_msg;
|
||||
use std::fs;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||
|
||||
const IN_MODULE_PATH: &str = "in-wasm-path";
|
||||
const INVOKE_ARG: &str = "arg";
|
||||
|
||||
fn prepare_args<'a, 'b>() -> [Arg<'a, 'b>; 2] {
|
||||
[
|
||||
Arg::with_name(IN_MODULE_PATH)
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.short("i")
|
||||
.help("path to the wasm file"),
|
||||
Arg::with_name(INVOKE_ARG)
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.short("a")
|
||||
.help("argument for the invoke function in the Wasm module"),
|
||||
]
|
||||
}
|
||||
|
||||
fn execute_wasm<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("execute")
|
||||
.about("Execute provided module on the Fluence Frank VM")
|
||||
.args(&prepare_args())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), ExitFailure> {
|
||||
let app = App::new("Fluence Frank Wasm execution environment for test purposes")
|
||||
.version(VERSION)
|
||||
.author(AUTHORS)
|
||||
.about(DESCRIPTION)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.subcommand(execute_wasm());
|
||||
|
||||
match app.get_matches().subcommand() {
|
||||
("execute", Some(arg)) => {
|
||||
let config = Box::new(Config::default());
|
||||
let in_module_path = arg.value_of(IN_MODULE_PATH).unwrap();
|
||||
let wasm_code =
|
||||
fs::read(in_module_path).unwrap_or_else(|err| panic!(format!("{}", err)));
|
||||
let wasm_code = prepare_module(&wasm_code, &config)
|
||||
.map_err(|e| panic!(format!("{}", e)))
|
||||
.unwrap();
|
||||
let invoke_arg = arg.value_of(INVOKE_ARG).unwrap();
|
||||
|
||||
let _ = Frank::new(&wasm_code, config)
|
||||
.map_err(|err| panic!(format!("{}", err)))
|
||||
.and_then(|mut executor| executor.0.invoke(invoke_arg.as_bytes()))
|
||||
.map_err(|err| panic!(format!("{}", err)))
|
||||
.map(|result| {
|
||||
let outcome_copy = result.outcome.clone();
|
||||
match String::from_utf8(result.outcome) {
|
||||
Ok(s) => println!("result: {}\nspent gas: {} ", s, result.spent_gas),
|
||||
Err(_) => println!(
|
||||
"result: {:?}\nspent gas: {} ",
|
||||
outcome_copy, result.spent_gas
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
c => Err(err_msg(format!("Unexpected command: {}", c.0)).into()),
|
||||
}
|
||||
}
|
137
vm/src/main/rust/src/vm/config.rs
Normal file
137
vm/src/main/rust/src/vm/config.rs
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// Defines the Config struct that is similar to vm/src/main/scala/fluence/vm/config/VmConfig.scala.
|
||||
use crate::vm::errors::FrankError;
|
||||
use jni::objects::{JObject, JString};
|
||||
use jni::JNIEnv;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Config {
|
||||
/// Count of Wasm memory pages that will be preallocated on the VM startup.
|
||||
/// Each Wasm pages is 65536 bytes long.
|
||||
pub mem_pages_count: i32,
|
||||
|
||||
/// If true, registers the logger Wasm module with name 'logger'.
|
||||
/// This functionality is just for debugging, and this module will be disabled in future.
|
||||
pub logger_enabled: bool,
|
||||
|
||||
/// Memory will be split by chunks to be able to build Merkle Tree on top of it.
|
||||
/// Size of memory in bytes must be dividable by chunkSize.
|
||||
pub chunk_size: i32,
|
||||
|
||||
/// The name of the main module handler function.
|
||||
pub invoke_function_name: String,
|
||||
|
||||
/// The name of function that should be called for allocation memory. This function
|
||||
/// is used for passing array of bytes to the main module.
|
||||
pub allocate_function_name: String,
|
||||
|
||||
/// The name of function that should be called for deallocation of
|
||||
/// previously allocated memory by allocateFunction.
|
||||
pub deallocate_function_name: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
// some reasonable defaults
|
||||
Self {
|
||||
// 65536*1600 ~ 100 Mb
|
||||
mem_pages_count: 1600,
|
||||
invoke_function_name: "invoke".to_string(),
|
||||
allocate_function_name: "allocate".to_string(),
|
||||
deallocate_function_name: "deallocate".to_string(),
|
||||
logger_enabled: true,
|
||||
chunk_size: 4096,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Creates a new config based on the supplied Scala object Config.
|
||||
/// This config should have the following structure:
|
||||
///
|
||||
/// ```
|
||||
/// case class MainModuleConfig(
|
||||
/// name: Option[String],
|
||||
/// allocateFunctionName: String,
|
||||
/// deallocateFunctionName: String,
|
||||
/// invokeFunctionName: String
|
||||
/// )
|
||||
///
|
||||
/// case class VmConfig(
|
||||
/// memPagesCount: Int,
|
||||
/// loggerEnabled: Boolean,
|
||||
/// chunkSize: Int,
|
||||
/// mainModuleConfig: MainModuleConfig
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
pub fn new(env: &JNIEnv, config: JObject) -> Result<Box<Self>, FrankError> {
|
||||
let mem_pages_count = env.call_method(config, "memPagesCount", "()I", &[])?.i()?;
|
||||
let logger_enabled = env.call_method(config, "loggerEnabled", "()Z", &[])?.z()?;
|
||||
let chunk_size = env.call_method(config, "chunkSize", "()I", &[])?.i()?;
|
||||
|
||||
let main_module_config = env
|
||||
.call_method(
|
||||
config,
|
||||
"mainModuleConfig",
|
||||
"()Lfluence/vm/config/MainModuleConfig;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
let allocate_function_name = env
|
||||
.call_method(
|
||||
main_module_config,
|
||||
"allocateFunctionName",
|
||||
"()Ljava/lang/String;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
let deallocate_function_name = env
|
||||
.call_method(
|
||||
main_module_config,
|
||||
"deallocateFunctionName",
|
||||
"()Ljava/lang/String;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
let invoke_function_name = env
|
||||
.call_method(
|
||||
main_module_config,
|
||||
"invokeFunctionName",
|
||||
"()Ljava/lang/String;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
// converts JObject to JString (without copying, just enum type changes)
|
||||
let allocate_function_name = env.get_string(JString::from(allocate_function_name))?;
|
||||
let deallocate_function_name = env.get_string(JString::from(deallocate_function_name))?;
|
||||
let invoke_function_name = env.get_string(JString::from(invoke_function_name))?;
|
||||
|
||||
Ok(Box::new(Self {
|
||||
mem_pages_count,
|
||||
logger_enabled,
|
||||
chunk_size,
|
||||
// and then finally to Rust String (requires one copy)
|
||||
invoke_function_name: String::from(invoke_function_name),
|
||||
allocate_function_name: String::from(allocate_function_name),
|
||||
deallocate_function_name: String::from(deallocate_function_name),
|
||||
}))
|
||||
}
|
||||
}
|
143
vm/src/main/rust/src/vm/errors.rs
Normal file
143
vm/src/main/rust/src/vm/errors.rs
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
use jni::errors::Error as JNIWrapperError;
|
||||
use wasmer_runtime::error::{
|
||||
CallError, CompileError, CreationError, Error, ResolveError, RuntimeError,
|
||||
};
|
||||
|
||||
// TODO: more errors to come (when preparation step will be completely landed)
|
||||
/// Errors related to the preparation (instrumentation and so on) and compilation by Wasmer steps.
|
||||
pub enum InitializationError {
|
||||
/// Error that raises during compilation Wasm code by Wasmer.
|
||||
WasmerCreationError(String),
|
||||
|
||||
/// Error that raises during creation of some Wasm objects (like table and memory) by Wasmer.
|
||||
WasmerCompileError(String),
|
||||
|
||||
/// Error that raises on the preparation step.
|
||||
PrepareError(String),
|
||||
}
|
||||
|
||||
pub enum FrankError {
|
||||
/// Errors related to the preparation (instrumentation and so on) and compilation by Wasmer steps.
|
||||
InstantiationError(String),
|
||||
|
||||
/// Errors related to parameter passing from Java to Rust and back.
|
||||
JNIError(String),
|
||||
|
||||
/// Errors for I/O errors raising while opening a file.
|
||||
IOError(String),
|
||||
|
||||
/// This error type is produced by Wasmer during resolving a Wasm function.
|
||||
WasmerResolveError(String),
|
||||
|
||||
/// Error related to calling a main Wasm module.
|
||||
WasmerInvokeError(String),
|
||||
|
||||
/// Error indicates that smth really bad happened (like removing the global Frank state).
|
||||
FrankNotInitialized,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InitializationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
InitializationError::WasmerCompileError(msg) => write!(f, "{}", msg),
|
||||
InitializationError::WasmerCreationError(msg) => write!(f, "{}", msg),
|
||||
InitializationError::PrepareError(msg) => write!(f, "{}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FrankError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
FrankError::InstantiationError(msg) => write!(f, "InstantiationError: {}", msg),
|
||||
FrankError::JNIError(msg) => write!(f, "JNIError: {}", msg),
|
||||
FrankError::IOError(msg) => write!(f, "IOError: {}", msg),
|
||||
FrankError::WasmerResolveError(msg) => write!(f, "WasmerResolveError: {}", msg),
|
||||
FrankError::WasmerInvokeError(msg) => write!(f, "WasmerInvokeError: {}", msg),
|
||||
FrankError::FrankNotInitialized => write!(
|
||||
f,
|
||||
"Attempt to use invoke virtual machine while it hasn't been initialized.\
|
||||
Please call the initialization method first."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreationError> for InitializationError {
|
||||
fn from(err: CreationError) -> Self {
|
||||
InitializationError::WasmerCreationError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CompileError> for InitializationError {
|
||||
fn from(err: CompileError) -> Self {
|
||||
InitializationError::WasmerCompileError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<parity_wasm::elements::Error> for InitializationError {
|
||||
fn from(err: parity_wasm::elements::Error) -> Self {
|
||||
InitializationError::PrepareError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InitializationError> for FrankError {
|
||||
fn from(err: InitializationError) -> Self {
|
||||
FrankError::InstantiationError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JNIWrapperError> for FrankError {
|
||||
fn from(err: JNIWrapperError) -> Self {
|
||||
FrankError::JNIError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallError> for FrankError {
|
||||
fn from(err: CallError) -> Self {
|
||||
match err {
|
||||
CallError::Resolve(err) => FrankError::WasmerResolveError(format!("{}", err)),
|
||||
CallError::Runtime(err) => FrankError::WasmerInvokeError(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolveError> for FrankError {
|
||||
fn from(err: ResolveError) -> Self {
|
||||
FrankError::WasmerResolveError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RuntimeError> for FrankError {
|
||||
fn from(err: RuntimeError) -> Self {
|
||||
FrankError::WasmerInvokeError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for FrankError {
|
||||
fn from(err: Error) -> Self {
|
||||
FrankError::WasmerInvokeError(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for FrankError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
FrankError::IOError(format!("{}", err))
|
||||
}
|
||||
}
|
206
vm/src/main/rust/src/vm/frank.rs
Normal file
206
vm/src/main/rust/src/vm/frank.rs
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
vm::modules::env_module::EnvModule,
|
||||
vm::config::Config,
|
||||
vm::errors::FrankError,
|
||||
vm::frank_result::FrankResult,
|
||||
};
|
||||
|
||||
use sha2::{digest::generic_array::GenericArray, digest::FixedOutput, Digest, Sha256};
|
||||
use std::ffi::c_void;
|
||||
use wasmer_runtime::{func, imports, instantiate, Ctx, Func, Instance};
|
||||
use failure::_core::marker::PhantomData;
|
||||
use wasmer_runtime_core::memory::ptr::{Array, WasmPtr};
|
||||
|
||||
pub struct Frank {
|
||||
instance: &'static Instance,
|
||||
|
||||
// It is safe to use unwrap() while calling these functions because Option is used here
|
||||
// to allow partially initialization of the struct. And all Option fields will contain
|
||||
// Some if invoking Frank::new is succeed.
|
||||
allocate: Option<Func<'static, i32, i32>>,
|
||||
deallocate: Option<Func<'static, (i32, i32), ()>>,
|
||||
invoke: Option<Func<'static, (i32, i32), i32>>,
|
||||
|
||||
_tag: PhantomData<&'static Instance>,
|
||||
}
|
||||
|
||||
impl Drop for Frank {
|
||||
// In normal situation this method should be called only while VM shutting down.
|
||||
fn drop(&mut self) {
|
||||
drop(self.allocate.as_ref());
|
||||
drop(self.deallocate.as_ref());
|
||||
drop(self.invoke.as_ref());
|
||||
drop(Box::from(self.instance));
|
||||
}
|
||||
}
|
||||
|
||||
// Waiting for new release of Wasmer with https://github.com/wasmerio/wasmer/issues/748.
|
||||
// It will allow to use lazy_static here. thread_local isn't suitable in our case because
|
||||
// it is difficult to guarantee that jni code will be called on the same thead context
|
||||
// every time from the Scala part.
|
||||
pub static mut FRANK: Option<Box<Frank>> = None;
|
||||
|
||||
// A little hack: exporting functions with this name means that this module expects Ethereum blocks.
|
||||
// Will be changed in the future.
|
||||
const ETH_FUNC_NAME: &str = "expects_eth";
|
||||
|
||||
impl Frank {
|
||||
/// Writes given value on the given address.
|
||||
fn write_to_mem(&mut self, address: usize, value: &[u8]) -> Result<(), FrankError> {
|
||||
let memory = self.instance.context().memory(0);
|
||||
|
||||
for (byte_id, cell) in memory.view::<u8>()[address as usize..(address + value.len())]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
cell.set(value[byte_id]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads given count of bytes from given address.
|
||||
fn read_result_from_mem(&self, address: usize) -> Result<Vec<u8>, FrankError> {
|
||||
let memory = self.instance.context().memory(0);
|
||||
|
||||
let mut result_size: usize = 0;
|
||||
|
||||
for (byte_id, cell) in memory.view::<u8>()[address..address + 4].iter().enumerate() {
|
||||
result_size |= (cell.get() as usize) << (8 * byte_id);
|
||||
}
|
||||
|
||||
let mut result = Vec::<u8>::with_capacity(result_size);
|
||||
for cell in memory.view()[(address + 4) as usize..(address + result_size + 4)].iter() {
|
||||
result.push(cell.get());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Invokes a main module supplying byte array and expecting byte array with some outcome back.
|
||||
pub fn invoke(&mut self, fn_argument: &[u8]) -> Result<FrankResult, FrankError> {
|
||||
// renew the state of the registered environment module to track spent gas and eic
|
||||
let env: &mut EnvModule = unsafe { &mut *(self.instance.context().data as *mut EnvModule) };
|
||||
env.renew_state();
|
||||
|
||||
// allocate memory for the given argument and write it to memory
|
||||
let argument_len = fn_argument.len() as i32;
|
||||
let argument_address = if argument_len != 0 {
|
||||
let address = self.allocate.as_ref().unwrap().call(argument_len)?;
|
||||
self.write_to_mem(address as usize, fn_argument)?;
|
||||
address
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// invoke a main module, read a result and deallocate it
|
||||
let result_address = self
|
||||
.invoke
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.call(argument_address, argument_len)?;
|
||||
let result = self.read_result_from_mem(result_address as _)?;
|
||||
self.deallocate
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.call(result_address, result.len() as i32)?;
|
||||
|
||||
let state = env.get_state();
|
||||
Ok(FrankResult::new(result, state.0, state.1))
|
||||
}
|
||||
|
||||
/// Computes the virtual machine state.
|
||||
pub fn compute_vm_state_hash(
|
||||
&mut self,
|
||||
) -> GenericArray<u8, <Sha256 as FixedOutput>::OutputSize> {
|
||||
let mut hasher = Sha256::new();
|
||||
let memory = self.instance.context().memory(0);
|
||||
|
||||
let wasm_ptr = WasmPtr::<u8, Array>::new(0 as _);
|
||||
let raw_mem = wasm_ptr
|
||||
.deref(memory, 0, (memory.size().bytes().0 - 1) as _)
|
||||
.expect("frank: internal error in compute_vm_state_hash");
|
||||
let raw_mem: &[u8] = unsafe { &*(raw_mem as *const [std::cell::Cell<u8>] as *const [u8]) };
|
||||
|
||||
hasher.input(raw_mem);
|
||||
hasher.result()
|
||||
}
|
||||
|
||||
/// Creates a new virtual machine executor.
|
||||
pub fn new(module: &[u8], config: Box<Config>) -> Result<(Self, bool), FrankError> {
|
||||
let env_state = move || {
|
||||
// allocate EnvModule on the heap
|
||||
let env_module = EnvModule::new();
|
||||
let dtor = (|data: *mut c_void| unsafe {
|
||||
drop(Box::from_raw(data as *mut EnvModule));
|
||||
}) as fn(*mut c_void);
|
||||
|
||||
// and then release corresponding Box object obtaining the raw pointer
|
||||
(Box::leak(env_module) as *mut EnvModule as *mut c_void, dtor)
|
||||
};
|
||||
|
||||
let import_objects = imports! {
|
||||
// this will enforce Wasmer to register EnvModule in the ctx.data field
|
||||
env_state,
|
||||
"logger" => {
|
||||
"log_utf8_string" => func!(logger_log_utf8_string),
|
||||
},
|
||||
"env" => {
|
||||
"gas" => func!(update_gas_counter),
|
||||
"eic" => func!(update_eic),
|
||||
},
|
||||
};
|
||||
|
||||
let instance: &'static mut Instance =
|
||||
Box::leak(Box::new(instantiate(module, &import_objects)?));
|
||||
let expects_eth = instance.func::<(), ()>(ETH_FUNC_NAME).is_ok();
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
instance,
|
||||
allocate: Some(instance.func::<(i32), i32>(&config.allocate_function_name)?),
|
||||
deallocate: Some(
|
||||
instance.func::<(i32, i32), ()>(&config.deallocate_function_name)?,
|
||||
),
|
||||
invoke: Some(instance.func::<(i32, i32), i32>(&config.invoke_function_name)?),
|
||||
_tag: PhantomData,
|
||||
},
|
||||
expects_eth,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Prints utf8 string of the given size from the given offset.
|
||||
fn logger_log_utf8_string(ctx: &mut Ctx, offset: i32, size: i32) {
|
||||
let wasm_ptr = WasmPtr::<u8, Array>::new(offset as _);
|
||||
match wasm_ptr.get_utf8_string(ctx.memory(0), size as _) {
|
||||
Some(msg) => print!("{}", msg),
|
||||
None => print!("frank logger: incorrect UTF8 string's been supplied to logger"),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_gas_counter(ctx: &mut Ctx, spent_gas: i32) {
|
||||
let env: &mut EnvModule = unsafe { &mut *(ctx.data as *mut EnvModule) };
|
||||
env.gas(spent_gas);
|
||||
}
|
||||
|
||||
fn update_eic(ctx: &mut Ctx, eic: i32) {
|
||||
let env: &mut EnvModule = unsafe { &mut *(ctx.data as *mut EnvModule) };
|
||||
env.eic(eic);
|
||||
}
|
42
vm/src/main/rust/src/vm/frank_result.rs
Normal file
42
vm/src/main/rust/src/vm/frank_result.rs
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FrankResult {
|
||||
pub outcome: Vec<u8>,
|
||||
pub spent_gas: i64,
|
||||
pub eic: i64,
|
||||
}
|
||||
|
||||
impl FrankResult {
|
||||
pub fn new(outcome: Vec<u8>, spent_gas: i64, eic: i64) -> Self {
|
||||
Self {
|
||||
outcome,
|
||||
spent_gas,
|
||||
eic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FrankResult {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
outcome: Vec::new(),
|
||||
spent_gas: 0,
|
||||
eic: 0,
|
||||
}
|
||||
}
|
||||
}
|
22
vm/src/main/rust/src/vm/mod.rs
Normal file
22
vm/src/main/rust/src/vm/mod.rs
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
pub mod config;
|
||||
pub mod errors;
|
||||
pub mod frank;
|
||||
pub mod frank_result;
|
||||
pub mod prepare;
|
||||
mod modules;
|
61
vm/src/main/rust/src/vm/modules/env_module.rs
Normal file
61
vm/src/main/rust/src/vm/modules/env_module.rs
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// Defines the environment module used for tracking execution state.
|
||||
use std::ops::AddAssign;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EnvModule {
|
||||
spent_gas: i64,
|
||||
eic: i64,
|
||||
}
|
||||
|
||||
impl EnvModule {
|
||||
pub fn new() -> Box<Self> {
|
||||
Box::new(Self {
|
||||
spent_gas: 0i64,
|
||||
eic: 0i64,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gas(&mut self, gas: i32) {
|
||||
// TODO: check for overflow
|
||||
self.spent_gas.add_assign(i64::from(gas));
|
||||
}
|
||||
|
||||
pub fn eic(&mut self, eic: i32) {
|
||||
// TODO: check for overflow
|
||||
self.eic.add_assign(i64::from(eic));
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> (i64, i64) {
|
||||
(self.spent_gas, self.eic)
|
||||
}
|
||||
|
||||
pub fn renew_state(&mut self) {
|
||||
self.spent_gas = 0;
|
||||
self.eic = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EnvModule {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spent_gas: 0i64,
|
||||
eic: 0i64,
|
||||
}
|
||||
}
|
||||
}
|
18
vm/src/main/rust/src/vm/modules/mod.rs
Normal file
18
vm/src/main/rust/src/vm/modules/mod.rs
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: more modules to come
|
||||
pub mod env_module;
|
83
vm/src/main/rust/src/vm/prepare.rs
Normal file
83
vm/src/main/rust/src/vm/prepare.rs
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Similar to
|
||||
// https://github.com/paritytech/substrate/blob/master/srml/contracts/src/wasm/prepare.rs
|
||||
// https://github.com/nearprotocol/nearcore/blob/master/runtime/near-vm-runner/src/prepare.rs
|
||||
|
||||
use parity_wasm::builder;
|
||||
use parity_wasm::elements;
|
||||
|
||||
use crate::vm::config::Config;
|
||||
use crate::vm::errors::InitializationError;
|
||||
use parity_wasm::elements::{MemorySection, MemoryType, ResizableLimits};
|
||||
|
||||
struct ModulePreparator {
|
||||
module: elements::Module,
|
||||
}
|
||||
|
||||
impl<'a> ModulePreparator {
|
||||
fn init(module_code: &[u8]) -> Result<Self, InitializationError> {
|
||||
let module = elements::deserialize_buffer(module_code)?;
|
||||
|
||||
Ok(Self { module })
|
||||
}
|
||||
|
||||
fn set_mem_pages_count(self, mem_pages_count: u32) -> Self {
|
||||
let Self { mut module } = self;
|
||||
|
||||
// At now, there is could be only one memory section, so
|
||||
// it needs just to extract previous initial page count, delete existing memory section
|
||||
let limits = match module.memory_section_mut() {
|
||||
Some(section) => match section.entries_mut().pop() {
|
||||
Some(entry) => *entry.limits(),
|
||||
None => ResizableLimits::new(0 as _, Some(mem_pages_count)),
|
||||
},
|
||||
None => ResizableLimits::new(0 as _, Some(mem_pages_count)),
|
||||
};
|
||||
|
||||
let memory_entry = MemoryType::new(limits.initial(), Some(mem_pages_count));
|
||||
|
||||
let mut default_mem_section = MemorySection::default();
|
||||
|
||||
// and create a new one
|
||||
module
|
||||
.memory_section_mut()
|
||||
.unwrap_or_else(|| &mut default_mem_section)
|
||||
.entries_mut()
|
||||
.push(memory_entry);
|
||||
|
||||
let builder = builder::from_module(module);
|
||||
|
||||
Self {
|
||||
module: builder.build(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_wasm(self) -> Result<Vec<u8>, InitializationError> {
|
||||
elements::serialize(self.module).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepares a Wasm module:
|
||||
/// - set memory page count
|
||||
/// - TODO: instrument module with gas counter
|
||||
/// - TODO: instrument module with eic
|
||||
pub fn prepare_module(module: &[u8], config: &Config) -> Result<Vec<u8>, InitializationError> {
|
||||
ModulePreparator::init(module)?
|
||||
.set_mem_pages_count(config.mem_pages_count as _)
|
||||
.to_wasm()
|
||||
}
|
25
vm/src/main/scala/fluence/vm/InvocationResult.scala
Normal file
25
vm/src/main/scala/fluence/vm/InvocationResult.scala
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
/**
|
||||
* Represents result of the VM invocation.
|
||||
*
|
||||
* @param output the computed result by Frank VM
|
||||
* @param spentGas spent gas by producing the output
|
||||
*/
|
||||
case class InvocationResult(output: Array[Byte], spentGas: Long)
|
27
vm/src/main/scala/fluence/vm/Utils.scala
Normal file
27
vm/src/main/scala/fluence/vm/Utils.scala
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
object Utils {
|
||||
|
||||
def getModuleDirPrefix(): String =
|
||||
// getProperty could return different path depends on the run method (Idea or sbt)
|
||||
if (System.getProperty("user.dir").endsWith("/vm"))
|
||||
System.getProperty("user.dir")
|
||||
else
|
||||
System.getProperty("user.dir") + "/vm/"
|
||||
}
|
100
vm/src/main/scala/fluence/vm/WasmVm.scala
Normal file
100
vm/src/main/scala/fluence/vm/WasmVm.scala
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import cats.data.{EitherT, NonEmptyList}
|
||||
import cats.effect.{LiftIO, Sync}
|
||||
import cats.Monad
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fluence.vm.config.VmConfig
|
||||
import fluence.vm.error.{InitializationError, InvocationError, StateComputationError}
|
||||
import fluence.vm.frank.{FrankAdapter, FrankWasmVm}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Virtual Machine api.
|
||||
*/
|
||||
trait WasmVm {
|
||||
|
||||
/**
|
||||
* Invokes Wasm ''function'' from specified Wasm ''module''. Each function receives and returns array of bytes.
|
||||
*
|
||||
* Note that, modules should be registered when VM started!
|
||||
*
|
||||
* @param fnArgument a Function arguments
|
||||
* @tparam F a monad with an ability to absorb 'IO'
|
||||
*/
|
||||
def invoke[F[_]: LiftIO: Monad](
|
||||
fnArgument: Array[Byte] = Array.emptyByteArray
|
||||
): EitherT[F, InvocationError, InvocationResult]
|
||||
|
||||
/**
|
||||
* Returns hash of all significant inner state of this VM. This function calculates
|
||||
* hashes for the state of each module and then concatenates them together.
|
||||
* It's behaviour will change in future, till it looks like this:
|
||||
* {{{
|
||||
* vmState = hash(hash(module1 state), hash(module2 state), ...))
|
||||
* }}}
|
||||
* '''Note!''' It's very expensive operation, try to avoid frequent use.
|
||||
*/
|
||||
def computeVmState[F[_]: LiftIO: Monad]: EitherT[F, StateComputationError, ByteVector]
|
||||
|
||||
/**
|
||||
* Temporary way to pass a flag from userland (the WASM file) to the Node, denotes whether an app
|
||||
* expects outer world to pass Ethereum blocks data into it.
|
||||
* TODO move this flag to the Smart Contract
|
||||
*/
|
||||
val expectsEth: Boolean
|
||||
}
|
||||
|
||||
object WasmVm {
|
||||
val javaLibPath: String = System.getProperty("java.library.path")
|
||||
println(s"java.library.path = $javaLibPath")
|
||||
|
||||
/**
|
||||
* Main method factory for building VM.
|
||||
* Compiles all files immediately by Asmble and returns VM implementation with eager module instantiation.
|
||||
*
|
||||
* @param inFiles input files in wasm or wast format
|
||||
* @param configNamespace a path of config in 'lightbend/config terms, please see reference.conf
|
||||
*/
|
||||
def apply[F[_]: Sync](
|
||||
inFiles: NonEmptyList[String],
|
||||
configNamespace: String = "fluence.vm.client",
|
||||
conf: ⇒ Config = ConfigFactory.load()
|
||||
): EitherT[F, InitializationError, WasmVm] =
|
||||
for {
|
||||
// reading config
|
||||
config ← VmConfig.readT[F](configNamespace, conf)
|
||||
|
||||
vmRunnerInvoker <- EitherT.right(Sync[F].delay(new FrankAdapter()))
|
||||
initializationResult <- EitherT.right(Sync[F].delay(vmRunnerInvoker.initialize(inFiles.head, config)))
|
||||
|
||||
_ ← EitherT.cond(
|
||||
initializationResult.error.isEmpty,
|
||||
(),
|
||||
InitializationError(initializationResult.error.get)
|
||||
)
|
||||
|
||||
} yield new FrankWasmVm(
|
||||
vmRunnerInvoker,
|
||||
initializationResult.expectsEth
|
||||
)
|
||||
|
||||
}
|
68
vm/src/main/scala/fluence/vm/config/VmConfig.scala
Normal file
68
vm/src/main/scala/fluence/vm/config/VmConfig.scala
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm.config
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.Monad
|
||||
import cats.syntax.either._
|
||||
import com.typesafe.config.Config
|
||||
import fluence.vm.error.InitializationError
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Main module settings.
|
||||
*
|
||||
* @param name a name of the main module (None means absence of name section in a Wasm module)
|
||||
* @param allocateFunctionName name of a function that should be called for allocation memory
|
||||
* (used for passing complex data structures)
|
||||
* @param deallocateFunctionName name of a function that should be called for deallocation
|
||||
* of previously allocated memory
|
||||
* @param invokeFunctionName name of main module handler function
|
||||
*/
|
||||
case class MainModuleConfig(
|
||||
name: Option[String],
|
||||
allocateFunctionName: String,
|
||||
deallocateFunctionName: String,
|
||||
invokeFunctionName: String
|
||||
)
|
||||
|
||||
/**
|
||||
* WasmVm settings.
|
||||
*
|
||||
* @param memPagesCount the maximum count of memory pages when a module doesn't say
|
||||
* @param loggerEnabled if set, registers the logger Wasm module with name 'logger'
|
||||
* @param chunkSize a size of the memory chunks, that memory will be split into
|
||||
* @param mainModuleConfig settings for the main module
|
||||
*/
|
||||
case class VmConfig(
|
||||
memPagesCount: Int,
|
||||
loggerEnabled: Boolean,
|
||||
chunkSize: Int,
|
||||
mainModuleConfig: MainModuleConfig
|
||||
)
|
||||
|
||||
object VmConfig {
|
||||
import net.ceedubs.ficus.Ficus._
|
||||
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
|
||||
|
||||
def readT[F[_]: Monad](namespace: String, conf: ⇒ Config): EitherT[F, InitializationError, VmConfig] = {
|
||||
EitherT
|
||||
.fromEither[F](Either.catchNonFatal(conf.getConfig(namespace).as[VmConfig]))
|
||||
.leftMap(e ⇒ InitializationError("Unable to parse the virtual machine config " + e))
|
||||
}
|
||||
}
|
55
vm/src/main/scala/fluence/vm/error/VmError.scala
Normal file
55
vm/src/main/scala/fluence/vm/error/VmError.scala
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm.error
|
||||
|
||||
import scala.util.control.NoStackTrace
|
||||
|
||||
/**
|
||||
* Base trait for errors occurred in Virtual machine.
|
||||
*
|
||||
* @param message detailed error message
|
||||
* @param causedBy caught [[Throwable]], if any
|
||||
*/
|
||||
sealed abstract class VmError(val message: String, val causedBy: Option[Throwable])
|
||||
extends Throwable(message, causedBy.orNull, true, false) with NoStackTrace
|
||||
|
||||
/**
|
||||
* Corresponds to errors occurred during VM initialization.
|
||||
*
|
||||
* @param message detailed error message
|
||||
* @param causedBy caught [[Throwable]], if any
|
||||
*/
|
||||
case class InitializationError(override val message: String, override val causedBy: Option[Throwable] = None)
|
||||
extends VmError(message, causedBy)
|
||||
|
||||
/**
|
||||
* Corresponds to errors occurred during VM function invocation.
|
||||
*
|
||||
* @param message detailed error message
|
||||
* @param causedBy caught [[Throwable]], if any
|
||||
*/
|
||||
case class InvocationError(override val message: String, override val causedBy: Option[Throwable] = None)
|
||||
extends VmError(message, causedBy)
|
||||
|
||||
/**
|
||||
* Corresponds to errors occurred during computing VM state hash.
|
||||
*
|
||||
* @param message detailed error message
|
||||
* @param causedBy caught [[Throwable]], if any
|
||||
*/
|
||||
case class StateComputationError(override val message: String, override val causedBy: Option[Throwable] = None)
|
||||
extends VmError(message, causedBy)
|
46
vm/src/main/scala/fluence/vm/frank/FrankAdapter.scala
Normal file
46
vm/src/main/scala/fluence/vm/frank/FrankAdapter.scala
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm.frank
|
||||
import fluence.vm.config.VmConfig
|
||||
import fluence.vm.frank.result.{RawInitializationResult, RawInvocationResult, RawStateComputationResult}
|
||||
import ch.jodersky.jni.nativeLoader
|
||||
|
||||
/**
|
||||
* Realizes connection to the virtual machine runner based on Wasmer through JNI.
|
||||
*/
|
||||
@nativeLoader("frank")
|
||||
class FrankAdapter {
|
||||
|
||||
/**
|
||||
* Initializes execution environment with given file path.
|
||||
*
|
||||
* @param filePath path to a wasm file
|
||||
*/
|
||||
@native def initialize(filePath: String, config: VmConfig): RawInitializationResult
|
||||
|
||||
/**
|
||||
* Invokes main module handler.
|
||||
*
|
||||
* @param arg argument for invoked module
|
||||
*/
|
||||
@native def invoke(arg: Array[Byte]): RawInvocationResult
|
||||
|
||||
/**
|
||||
* Returns hash of all significant inner state of the VM.
|
||||
*/
|
||||
@native def computeVmState(): RawStateComputationResult
|
||||
}
|
65
vm/src/main/scala/fluence/vm/frank/FrankWasmVm.scala
Normal file
65
vm/src/main/scala/fluence/vm/frank/FrankWasmVm.scala
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm.frank
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.either._
|
||||
import cats.effect.{IO, LiftIO}
|
||||
import fluence.vm.{InvocationResult, WasmVm}
|
||||
import scodec.bits.ByteVector
|
||||
import fluence.vm.error.{InvocationError, StateComputationError}
|
||||
import fluence.vm.frank.result.{RawInvocationResult, RawStateComputationResult}
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Base implementation of [[WasmVm]] based on the Wasmer execution environment.
|
||||
*
|
||||
* '''Note!!! This implementation isn't thread-safe. The provision of calls
|
||||
* linearization is the task of the caller side.'''
|
||||
*/
|
||||
class FrankWasmVm(
|
||||
private val vmRunnerInvoker: FrankAdapter,
|
||||
val expectsEth: Boolean
|
||||
) extends WasmVm {
|
||||
|
||||
override def invoke[F[_]: LiftIO: Monad](
|
||||
fnArgument: Array[Byte]
|
||||
): EitherT[F, InvocationError, InvocationResult] =
|
||||
EitherT(
|
||||
IO(vmRunnerInvoker.invoke(fnArgument)).attempt
|
||||
.to[F]
|
||||
).leftMap(e ⇒ InvocationError(s"Frank invocation failed by exception. Cause: ${e.getMessage}", Some(e)))
|
||||
.subflatMap {
|
||||
case RawInvocationResult(Some(err), _, _) ⇒
|
||||
InvocationError(s"Frank invocation failed. Cause: $err").asLeft[InvocationResult]
|
||||
case RawInvocationResult(None, output, spentGas) ⇒
|
||||
InvocationResult(output, spentGas).asRight[InvocationError]
|
||||
}
|
||||
|
||||
override def computeVmState[F[_]: LiftIO: Monad]: EitherT[F, StateComputationError, ByteVector] =
|
||||
EitherT(
|
||||
IO(vmRunnerInvoker.computeVmState()).attempt
|
||||
.to[F]
|
||||
).leftMap(e ⇒ StateComputationError(s"Frank getting VM state failed. Cause: ${e.getMessage}", Some(e))).subflatMap {
|
||||
case RawStateComputationResult(Some(err), _) ⇒
|
||||
StateComputationError(s"Frank invocation failed. Cause: $err").asLeft[ByteVector]
|
||||
case RawStateComputationResult(None, state) ⇒
|
||||
ByteVector(state).asRight[StateComputationError]
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm.frank.result
|
||||
|
||||
/**
|
||||
* Represents raw JNI result of FrankAdapter::initialize invoking.
|
||||
*
|
||||
* @param error represent various initialization errors, None - no error occurred
|
||||
*/
|
||||
final case class RawInitializationResult(error: Option[String], expectsEth: Boolean)
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm.frank.result
|
||||
|
||||
/**
|
||||
* Represents raw JNI result of FrankAdapter::invoke invoking.
|
||||
*
|
||||
* @param error represents various invocation errors, None - no error occurred
|
||||
* @param output the computed result by Frank VM, valid only if no error occurred (error == None)
|
||||
* @param spentGas spent gas by producing the output, valid only if no error occurred (error == None)
|
||||
*/
|
||||
final case class RawInvocationResult(error: Option[String], output: Array[Byte], spentGas: Long)
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm.frank.result
|
||||
|
||||
/**
|
||||
* Represents raw JNI result of FrankAdapter::computeVmState invoking.
|
||||
*
|
||||
* @param error represent various initialization errors, None - no error occurred
|
||||
* @param state computed state of Frank VM, valid only if no error occurred (error == None)
|
||||
*/
|
||||
final case class RawStateComputationResult(error: Option[String], state: Array[Byte])
|
41
vm/src/main/scala/fluence/vm/package.scala
Normal file
41
vm/src/main/scala/fluence/vm/package.scala
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence
|
||||
import cats.Functor
|
||||
import cats.data.EitherT
|
||||
import fluence.vm.error.VmError
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
package object vm {
|
||||
|
||||
implicit class VmErrorMapper[F[_]: Functor, E <: VmError, T](eitherT: EitherT[F, E, T]) {
|
||||
|
||||
def toVmError: EitherT[F, VmError, T] = {
|
||||
eitherT.leftMap { e: VmError ⇒
|
||||
e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object eitherT {
|
||||
implicit class EitherTOps[F[_]: Functor, A, B](ef: F[Either[A, B]]) {
|
||||
def eitherT: EitherT[F, A, B] = EitherT(ef)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
mock-maker-inline
|
||||
# allows to mock final classes from 'asmble' project for testing
|
24
vm/src/test/resources/wast/bad-allocation-function-f64.wast
Normal file
24
vm/src/test/resources/wast/bad-allocation-function-f64.wast
Normal file
@ -0,0 +1,24 @@
|
||||
;; this example has "bad" allocation function that returns offset out of ByteBuffer limits as f64
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result f64)
|
||||
;; returns floating-point f64 number instead od integer
|
||||
(f64.const 200000000.12345)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
(func (export "invoke") (param $0 i32 ) (param $1 i32) (result i32)
|
||||
;; simply returns 10000
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
)
|
25
vm/src/test/resources/wast/bad-allocation-function-i64.wast
Normal file
25
vm/src/test/resources/wast/bad-allocation-function-i64.wast
Normal file
@ -0,0 +1,25 @@
|
||||
;; this example has "bad" allocation function that returns offset out of ByteBuffer limits as i64
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i64)
|
||||
;; returns maximum value of signed 64-bit integer that wittingly exceeds maximum ByteBuffer size
|
||||
;; (and the address space limit on amd64 architecture)
|
||||
(i64.const 9223372036854775807)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
(func (export "invoke") (param $0 i32 ) (param $1 i32) (result i32)
|
||||
;; simply returns 10000
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
)
|
77
vm/src/test/resources/wast/counter-copy.wast
Normal file
77
vm/src/test/resources/wast/counter-copy.wast
Normal file
@ -0,0 +1,77 @@
|
||||
;; copy of simple counter module with different name
|
||||
|
||||
(module $CounterCopyModule
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;;
|
||||
;; Initializes counter with zero value
|
||||
;;
|
||||
(data (i32.const 12) "\00\00\00\00")
|
||||
|
||||
;;
|
||||
;; Increments variable in memory by one and returns it.
|
||||
;;
|
||||
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
|
||||
(i32.store offset=12
|
||||
(i32.const 0)
|
||||
(i32.add
|
||||
(i32.load offset=12 (i32.const 0))
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
(call $putIntResult
|
||||
(i32.load offset=12 (i32.const 0))
|
||||
)
|
||||
)
|
||||
|
||||
;; int putIntResult(int result) {
|
||||
;; const int address = 1024*1024;
|
||||
;;
|
||||
;; globalBuffer[address] = 0;
|
||||
;; globalBuffer[address + 1] = 0;
|
||||
;; globalBuffer[address + 2] = 0;
|
||||
;; globalBuffer[address + 3] = 4;
|
||||
;;
|
||||
;; for(int i = 0; i < 4; ++i) {
|
||||
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
|
||||
;; }
|
||||
;;
|
||||
;; return address;
|
||||
;; }
|
||||
(func $putIntResult (param $result i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
|
||||
(set_local $1 (i32.const 1048596))
|
||||
(loop $label$0
|
||||
(i32.store8
|
||||
(get_local $1)
|
||||
(i32.shr_u (get_local $result) (get_local $2))
|
||||
)
|
||||
(set_local $1
|
||||
(i32.add (get_local $1) (i32.const 1))
|
||||
)
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
|
||||
(i32.const 32)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
77
vm/src/test/resources/wast/counter.wast
Normal file
77
vm/src/test/resources/wast/counter.wast
Normal file
@ -0,0 +1,77 @@
|
||||
;; simple in-memory counter
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;;
|
||||
;; Initializes counter with zero value
|
||||
;;
|
||||
(data (i32.const 12) "\00\00\00\00")
|
||||
|
||||
;;
|
||||
;; Increments variable in memory by one and returns it.
|
||||
;;
|
||||
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
|
||||
(i32.store offset=12
|
||||
(i32.const 0)
|
||||
(i32.add
|
||||
(i32.load offset=12 (i32.const 0))
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
(call $putIntResult
|
||||
(i32.load offset=12 (i32.const 0))
|
||||
)
|
||||
)
|
||||
|
||||
;; int putIntResult(int result) {
|
||||
;; const int address = 1024*1024;
|
||||
;;
|
||||
;; globalBuffer[address] = 0;
|
||||
;; globalBuffer[address + 1] = 0;
|
||||
;; globalBuffer[address + 2] = 0;
|
||||
;; globalBuffer[address + 3] = 4;
|
||||
;;
|
||||
;; for(int i = 0; i < 4; ++i) {
|
||||
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
|
||||
;; }
|
||||
;;
|
||||
;; return address;
|
||||
;; }
|
||||
(func $putIntResult (param $result i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
|
||||
(set_local $1 (i32.const 1048596))
|
||||
(loop $label$0
|
||||
(i32.store8
|
||||
(get_local $1)
|
||||
(i32.shr_u (get_local $result) (get_local $2))
|
||||
)
|
||||
(set_local $1
|
||||
(i32.add (get_local $1) (i32.const 1))
|
||||
)
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
|
||||
(i32.const 32)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
108
vm/src/test/resources/wast/incorrect-array-returning.wast
Normal file
108
vm/src/test/resources/wast/incorrect-array-returning.wast
Normal file
@ -0,0 +1,108 @@
|
||||
;; this example has some functions that recieve and put strings
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(data 0 (offset (i32.const 128)) "Hello from Fluence Labs!\00")
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; puts 0x00FFFFFF as result size in memory at offset 1048592 and returns pointer to it
|
||||
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
|
||||
(local $0 i32)
|
||||
(set_local $0 (i32.const 1048592))
|
||||
|
||||
(i32.store8
|
||||
(get_local $0)
|
||||
(i32.const 255)
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $0) (i32.const 1))
|
||||
(i32.const 255)
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $0) (i32.const 2))
|
||||
(i32.const 255)
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $0) (i32.const 3))
|
||||
(i32.const 0)
|
||||
)
|
||||
|
||||
(i32.const 1048592)
|
||||
)
|
||||
|
||||
;; int putStringResult(const char *string, int stringSize, int address) {
|
||||
;;
|
||||
;; globalBuffer[address] = (stringSize >> 24) & 0xFF;
|
||||
;; globalBuffer[address + 1] = (stringSize >> 16) & 0xFF;
|
||||
;; globalBuffer[address + 2] = (stringSize >> 8) & 0xFF;
|
||||
;; globalBuffer[address + 3] = stringSize & 0xFF;
|
||||
;;
|
||||
;; for(int i = 0; i < stringSize; ++i) {
|
||||
;; globalBuffer[address + 4 + i] = string[i];
|
||||
;; }
|
||||
;; }
|
||||
(func $putStringResult (param $string i32) (param $stringSize i32) (param $address i32) (result i32)
|
||||
(local $3 i32)
|
||||
(local $4 i32)
|
||||
|
||||
(i32.store8
|
||||
(get_local $address)
|
||||
(get_local $stringSize)
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 1))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 8))
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 2))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 16))
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 3))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 24))
|
||||
)
|
||||
|
||||
(set_local $3 (get_local $address))
|
||||
(set_local $address (i32.add (get_local $address) (i32.const 4)))
|
||||
|
||||
(loop $label$0
|
||||
;; globalBuffer[address + 4 + i] = string[i];
|
||||
(i32.store8
|
||||
(get_local $address)
|
||||
(i32.load8_u (get_local $string))
|
||||
)
|
||||
|
||||
;; ++string
|
||||
(set_local $string
|
||||
(i32.add (get_local $string) (i32.const 1))
|
||||
)
|
||||
|
||||
;; ++globalBuffer
|
||||
(set_local $address
|
||||
(i32.add (get_local $address) (i32.const 1))
|
||||
)
|
||||
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $4 (i32.add (get_local $4) (i32.const 1)))
|
||||
(get_local $stringSize)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(get_local $3)
|
||||
)
|
||||
)
|
149
vm/src/test/resources/wast/mul.wast
Normal file
149
vm/src/test/resources/wast/mul.wast
Normal file
@ -0,0 +1,149 @@
|
||||
;; this example simply returns product of two integers
|
||||
|
||||
(module $MulModule
|
||||
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; int extractInt(const char *buffer, int begin, int end) {
|
||||
;; int value = 0;
|
||||
;; const int size = end - begin;
|
||||
;;
|
||||
;; // it is assumed that bytes already in little endian
|
||||
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
|
||||
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
|
||||
;; }
|
||||
;;
|
||||
;; return value;
|
||||
;; }
|
||||
(func $extractInt (param $buffer i32) (param $begin i32) (param $end i32) (result i32)
|
||||
(local $3 i32)
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.lt_s
|
||||
(tee_local $3
|
||||
(i32.sub (get_local $end) (get_local $begin))
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
|
||||
(set_local $begin
|
||||
(i32.add (get_local $buffer) (get_local $begin))
|
||||
)
|
||||
|
||||
(set_local $end (i32.const 0))
|
||||
(set_local $buffer (i32.const 0))
|
||||
(loop $label$1
|
||||
(set_local $buffer
|
||||
(i32.or
|
||||
(i32.shl
|
||||
(i32.load8_s (get_local $begin))
|
||||
(get_local $end)
|
||||
)
|
||||
(get_local $buffer)
|
||||
)
|
||||
)
|
||||
(set_local $begin
|
||||
(i32.add (get_local $begin) (i32.const 1))
|
||||
)
|
||||
(set_local $end
|
||||
(i32.add (get_local $end) (i32.const 8))
|
||||
)
|
||||
(br_if $label$1
|
||||
(tee_local $3
|
||||
(i32.add (get_local $3) (i32.const -1))
|
||||
)
|
||||
)
|
||||
)
|
||||
(return (get_local $buffer))
|
||||
)
|
||||
(i32.const 0)
|
||||
)
|
||||
|
||||
;; int invoke(const char *buffer, int size) {
|
||||
;; if(size != 8) {
|
||||
;; return 0;
|
||||
;; }
|
||||
;;
|
||||
;; const int a = extractInt(buffer, 0, 4);
|
||||
;; const int b = extractInt(buffer, 4, 8);
|
||||
;;
|
||||
;; return a * b;
|
||||
;; }
|
||||
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.ne (get_local $size) (i32.const 8))
|
||||
)
|
||||
(set_local $2
|
||||
(i32.mul
|
||||
(call $extractInt
|
||||
(get_local $buffer)
|
||||
(i32.const 0)
|
||||
(i32.const 4)
|
||||
)
|
||||
(call $extractInt
|
||||
(get_local $buffer)
|
||||
(i32.const 4)
|
||||
(i32.const 8)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call $putIntResult (get_local $2))
|
||||
)
|
||||
|
||||
;; int putIntResult(int result) {
|
||||
;; const int address = 1024*1024;
|
||||
;;
|
||||
;; globalBuffer[address] = 0;
|
||||
;; globalBuffer[address + 1] = 0;
|
||||
;; globalBuffer[address + 2] = 0;
|
||||
;; globalBuffer[address + 3] = 4;
|
||||
;;
|
||||
;; for(int i = 0; i < 4; ++i) {
|
||||
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
|
||||
;; }
|
||||
;;
|
||||
;; return address;
|
||||
;; }
|
||||
(func $putIntResult (param $result i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
|
||||
(set_local $1 (i32.const 1048596))
|
||||
(loop $label$0
|
||||
(i32.store8
|
||||
(get_local $1)
|
||||
(i32.shr_u (get_local $result) (get_local $2))
|
||||
)
|
||||
(set_local $1
|
||||
(i32.add (get_local $1) (i32.const 1))
|
||||
)
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
|
||||
(i32.const 32)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
20
vm/src/test/resources/wast/no-getMemory.wast
Normal file
20
vm/src/test/resources/wast/no-getMemory.wast
Normal file
@ -0,0 +1,20 @@
|
||||
;; this example has allocate/deallocate functions but doesn't have memory sections.
|
||||
;; Asmble version 0.4.0 doesn't generate getMemory function in this case.
|
||||
|
||||
(module
|
||||
(func (export "allocate") (param $0 i32 ) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
(func (export "invoke") (param $0 i32) (param $1 i32) (result i32)
|
||||
;; simply returns 10000
|
||||
(i32.const 10000)
|
||||
)
|
||||
)
|
19
vm/src/test/resources/wast/no-invoke.wast
Normal file
19
vm/src/test/resources/wast/no-invoke.wast
Normal file
@ -0,0 +1,19 @@
|
||||
;; this example has allocate/deallocate functions but doesn't have invoke function.
|
||||
|
||||
(module
|
||||
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
)
|
117
vm/src/test/resources/wast/simple-array-mutation.wast
Normal file
117
vm/src/test/resources/wast/simple-array-mutation.wast
Normal file
@ -0,0 +1,117 @@
|
||||
;; this example adds 1 to each byte in supplied string
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; char* invoke(const char *array, int arraySize) {
|
||||
;; for(int i = 0; i < stringSize; ++i) {
|
||||
;; ++array[i];
|
||||
;; }
|
||||
;;
|
||||
;; return array;
|
||||
;; }
|
||||
(func (export "invoke") (param $array i32) (param $arraySize i32) (result i32)
|
||||
(local $arrayIdx i32)
|
||||
(loop $label$0
|
||||
(i32.store8
|
||||
(i32.add (get_local $array) (get_local $arrayIdx))
|
||||
(i32.add
|
||||
(i32.load8_s
|
||||
(i32.add (get_local $array) (get_local $arrayIdx))
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $arrayIdx
|
||||
(i32.add (get_local $arrayIdx) (i32.const 1))
|
||||
)
|
||||
(get_local $arraySize)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(call $putArrayResult
|
||||
(get_local $array)
|
||||
(get_local $arraySize)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
||||
|
||||
;; int putArrayResult(const char *string, int stringSize, int address) {
|
||||
;;
|
||||
;; globalBuffer[address] = (stringSize >> 24) & 0xFF;
|
||||
;; globalBuffer[address + 1] = (stringSize >> 16) & 0xFF;
|
||||
;; globalBuffer[address + 2] = (stringSize >> 8) & 0xFF;
|
||||
;; globalBuffer[address + 3] = stringSize & 0xFF;
|
||||
;;
|
||||
;; for(int i = 0; i < stringSize; ++i) {
|
||||
;; globalBuffer[address + 4 + i] = string[i];
|
||||
;; }
|
||||
;; }
|
||||
(func $putArrayResult (param $string i32) (param $stringSize i32) (param $address i32) (result i32)
|
||||
(local $3 i32)
|
||||
(local $4 i32)
|
||||
|
||||
(i32.store8
|
||||
(get_local $address)
|
||||
(get_local $stringSize)
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 1))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 8))
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 2))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 16))
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 3))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 24))
|
||||
)
|
||||
|
||||
(set_local $3 (get_local $address))
|
||||
(set_local $address (i32.add (get_local $address) (i32.const 4)))
|
||||
|
||||
(loop $label$0
|
||||
;; globalBuffer[address + 4 + i] = string[i];
|
||||
(i32.store8
|
||||
(get_local $address)
|
||||
(i32.load8_u (get_local $string))
|
||||
)
|
||||
|
||||
;; ++string
|
||||
(set_local $string
|
||||
(i32.add (get_local $string) (i32.const 1))
|
||||
)
|
||||
|
||||
;; ++globalBuffer
|
||||
(set_local $address
|
||||
(i32.add (get_local $address) (i32.const 1))
|
||||
)
|
||||
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $4 (i32.add (get_local $4) (i32.const 1)))
|
||||
(get_local $stringSize)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(get_local $3)
|
||||
)
|
||||
)
|
92
vm/src/test/resources/wast/simple-array-returning.wast
Normal file
92
vm/src/test/resources/wast/simple-array-returning.wast
Normal file
@ -0,0 +1,92 @@
|
||||
;; this example simply returns pointer to "Hello from Fluence Labs!\00" string
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(data 0 (offset (i32.const 128)) "Hello from Fluence Labs!\00")
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; returns pointer to const string from memory
|
||||
(func (export "invoke") (param $buffer i32) (param $bufferSize i32) (result i32)
|
||||
(call $putArrayResult
|
||||
(i32.const 128)
|
||||
(i32.const 24)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
||||
|
||||
;; int putArrayResult(const char *string, int stringSize, int address) {
|
||||
;;
|
||||
;; globalBuffer[address] = (stringSize >> 24) & 0xFF;
|
||||
;; globalBuffer[address + 1] = (stringSize >> 16) & 0xFF;
|
||||
;; globalBuffer[address + 2] = (stringSize >> 8) & 0xFF;
|
||||
;; globalBuffer[address + 3] = stringSize & 0xFF;
|
||||
;;
|
||||
;; for(int i = 0; i < stringSize; ++i) {
|
||||
;; globalBuffer[address + 4 + i] = string[i];
|
||||
;; }
|
||||
;; }
|
||||
(func $putArrayResult (param $string i32) (param $stringSize i32) (param $address i32) (result i32)
|
||||
(local $3 i32)
|
||||
(local $4 i32)
|
||||
|
||||
(i32.store8
|
||||
(get_local $address)
|
||||
(get_local $stringSize)
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 1))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 8))
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 2))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 16))
|
||||
)
|
||||
(i32.store8
|
||||
(i32.add (get_local $address) (i32.const 3))
|
||||
(i32.shr_u (get_local $stringSize) (i32.const 24))
|
||||
)
|
||||
|
||||
(set_local $3 (get_local $address))
|
||||
(set_local $address (i32.add (get_local $address) (i32.const 4)))
|
||||
|
||||
(loop $label$0
|
||||
;; globalBuffer[address + 4 + i] = string[i];
|
||||
(i32.store8
|
||||
(get_local $address)
|
||||
(i32.load8_u (get_local $string))
|
||||
)
|
||||
|
||||
;; ++string
|
||||
(set_local $string
|
||||
(i32.add (get_local $string) (i32.const 1))
|
||||
)
|
||||
|
||||
;; ++globalBuffer
|
||||
(set_local $address
|
||||
(i32.add (get_local $address) (i32.const 1))
|
||||
)
|
||||
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $4 (i32.add (get_local $4) (i32.const 1)))
|
||||
(get_local $stringSize)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(get_local $3)
|
||||
)
|
||||
)
|
90
vm/src/test/resources/wast/simple-string-passing.wast
Normal file
90
vm/src/test/resources/wast/simple-string-passing.wast
Normal file
@ -0,0 +1,90 @@
|
||||
;; this example calculates circular xor of supplied buffer
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; int invoke(const char *buffer, int size) {
|
||||
;; int value = 0;
|
||||
;;
|
||||
;; for(int byteId = 0; byteId < size; ++byteId) {
|
||||
;; value ^= buffer[byteId];
|
||||
;; }
|
||||
;;
|
||||
;; return value;
|
||||
;; }
|
||||
(func (export "invoke") (param $buffer i32 ) (param $size i32) (result i32)
|
||||
(local $value i32)
|
||||
(set_local $value (i32.const 0) )
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.lt_s (get_local $size) (i32.const 1) )
|
||||
)
|
||||
|
||||
(loop $label$1
|
||||
(set_local $value
|
||||
(i32.xor (get_local $value) (i32.load8_s (get_local $buffer) ) )
|
||||
)
|
||||
(set_local $buffer
|
||||
(i32.add (get_local $buffer) (i32.const 1) )
|
||||
)
|
||||
(br_if $label$1
|
||||
(tee_local $size
|
||||
(i32.add (get_local $size) (i32.const -1) )
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call $putIntResult (get_local $value))
|
||||
)
|
||||
|
||||
;; int putIntResult(int result) {
|
||||
;; const int address = 1024*1024;
|
||||
;;
|
||||
;; globalBuffer[address] = 0;
|
||||
;; globalBuffer[address + 1] = 0;
|
||||
;; globalBuffer[address + 2] = 0;
|
||||
;; globalBuffer[address + 3] = 4;
|
||||
;;
|
||||
;; for(int i = 0; i < 4; ++i) {
|
||||
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
|
||||
;; }
|
||||
;;
|
||||
;; return address;
|
||||
;; }
|
||||
(func $putIntResult (param $result i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
|
||||
(set_local $1 (i32.const 1048596))
|
||||
(loop $label$0
|
||||
(i32.store8
|
||||
(get_local $1)
|
||||
(i32.shr_u (get_local $result) (get_local $2))
|
||||
)
|
||||
(set_local $1
|
||||
(i32.add (get_local $1) (i32.const 1))
|
||||
)
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
|
||||
(i32.const 32)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
169
vm/src/test/resources/wast/sum-copy.wast
Normal file
169
vm/src/test/resources/wast/sum-copy.wast
Normal file
@ -0,0 +1,169 @@
|
||||
;; copy of sum module with the same module name
|
||||
|
||||
(module $SumModule
|
||||
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; int extractInt(const char *buffer, int begin, int end) {
|
||||
;; int value = 0;
|
||||
;; const int size = end - begin;
|
||||
;;
|
||||
;; // it is assumed that bytes already in little endian
|
||||
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
|
||||
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
|
||||
;; }
|
||||
;;
|
||||
;; return value;
|
||||
;; }
|
||||
(func $extractInt (param $buffer i32) (param $begin i32) (param $end i32) (result i32)
|
||||
(local $3 i32)
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.lt_s
|
||||
(tee_local $3
|
||||
(i32.sub (get_local $end) (get_local $begin))
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
|
||||
(set_local $begin
|
||||
(i32.add (get_local $buffer) (get_local $begin))
|
||||
)
|
||||
|
||||
(set_local $end (i32.const 0))
|
||||
(set_local $buffer (i32.const 0))
|
||||
(loop $label$1
|
||||
(set_local $buffer
|
||||
(i32.or
|
||||
(i32.shl
|
||||
(i32.load8_s (get_local $begin))
|
||||
(get_local $end)
|
||||
)
|
||||
(get_local $buffer)
|
||||
)
|
||||
)
|
||||
(set_local $begin
|
||||
(i32.add (get_local $begin) (i32.const 1))
|
||||
)
|
||||
(set_local $end
|
||||
(i32.add (get_local $end) (i32.const 8))
|
||||
)
|
||||
(br_if $label$1
|
||||
(tee_local $3
|
||||
(i32.add (get_local $3) (i32.const -1))
|
||||
)
|
||||
)
|
||||
)
|
||||
(return (get_local $buffer))
|
||||
)
|
||||
(i32.const 0)
|
||||
)
|
||||
|
||||
;; int extractInt(const char *buffer, int begin, int end) {
|
||||
;; int value = 0;
|
||||
;; const int size = end - begin;
|
||||
;;
|
||||
;; // it is assumed that bytes already in little endian
|
||||
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
|
||||
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
|
||||
;; }
|
||||
;;
|
||||
;; return value;
|
||||
;; }
|
||||
(func $extractInt (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
|
||||
(local $3 i32)
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.lt_s
|
||||
(tee_local $3
|
||||
(i32.sub (get_local $2) (get_local $1))
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
|
||||
(set_local $1
|
||||
(i32.add (get_local $0) (get_local $1))
|
||||
)
|
||||
|
||||
(set_local $2 (i32.const 0))
|
||||
(set_local $0 (i32.const 0))
|
||||
(loop $label$1
|
||||
(set_local $0
|
||||
(i32.or
|
||||
(i32.shl
|
||||
(i32.load8_s (get_local $1))
|
||||
(get_local $2)
|
||||
)
|
||||
(get_local $0)
|
||||
)
|
||||
)
|
||||
(set_local $1
|
||||
(i32.add (get_local $1) (i32.const 1))
|
||||
)
|
||||
(set_local $2
|
||||
(i32.add (get_local $2) (i32.const 8))
|
||||
)
|
||||
(br_if $label$1
|
||||
(tee_local $3
|
||||
(i32.add (get_local $3) (i32.const -1))
|
||||
)
|
||||
)
|
||||
)
|
||||
(return (get_local $0))
|
||||
)
|
||||
(i32.const 0)
|
||||
)
|
||||
|
||||
;; int putIntResult(int result) {
|
||||
;; const int address = 1024*1024;
|
||||
;;
|
||||
;; globalBuffer[address] = 0;
|
||||
;; globalBuffer[address + 1] = 0;
|
||||
;; globalBuffer[address + 2] = 0;
|
||||
;; globalBuffer[address + 3] = 4;
|
||||
;;
|
||||
;; for(int i = 0; i < 4; ++i) {
|
||||
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
|
||||
;; }
|
||||
;;
|
||||
;; return address;
|
||||
;; }
|
||||
(func $putIntResult (param $result i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
|
||||
(set_local $1 (i32.const 1048596))
|
||||
(loop $label$0
|
||||
(i32.store8
|
||||
(get_local $1)
|
||||
(i32.shr_u (get_local $result) (get_local $2))
|
||||
)
|
||||
(set_local $1
|
||||
(i32.add (get_local $1) (i32.const 1))
|
||||
)
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
|
||||
(i32.const 32)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
25
vm/src/test/resources/wast/sum-with-trap.wast
Normal file
25
vm/src/test/resources/wast/sum-with-trap.wast
Normal file
@ -0,0 +1,25 @@
|
||||
;; this example simply returns
|
||||
|
||||
(module
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 10000)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; int sum(int a, int b) {
|
||||
;; return a + b;
|
||||
;; }
|
||||
(func (export "invoke") (param $0 i32) (param $1 i32) (result i32)
|
||||
(unreachable) ;; unreachable: An instruction which always traps.
|
||||
)
|
||||
)
|
149
vm/src/test/resources/wast/sum.wast
Normal file
149
vm/src/test/resources/wast/sum.wast
Normal file
@ -0,0 +1,149 @@
|
||||
;; this example compute sum of two given integers
|
||||
|
||||
(module
|
||||
|
||||
;; force Asmble to use memory
|
||||
(memory $0 20)
|
||||
(export "memory" (memory $0))
|
||||
|
||||
(func (export "allocate") (param $0 i32) (result i32)
|
||||
;; just return constant offset in ByteBuffer
|
||||
(i32.const 0)
|
||||
)
|
||||
|
||||
(func (export "deallocate") (param $address i32) (param $size i32) (return)
|
||||
;; in this simple example deallocation function does nothing
|
||||
(drop)
|
||||
(drop)
|
||||
)
|
||||
|
||||
;; int extractInt(const char *buffer, int begin, int end) {
|
||||
;; int value = 0;
|
||||
;; const int size = end - begin;
|
||||
;;
|
||||
;; // it is assumed that bytes already in little endian
|
||||
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
|
||||
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
|
||||
;; }
|
||||
;;
|
||||
;; return value;
|
||||
;; }
|
||||
(func $extractInt (param $buffer i32) (param $begin i32) (param $end i32) (result i32)
|
||||
(local $3 i32)
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.lt_s
|
||||
(tee_local $3
|
||||
(i32.sub (get_local $end) (get_local $begin))
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
|
||||
(set_local $begin
|
||||
(i32.add (get_local $buffer) (get_local $begin))
|
||||
)
|
||||
|
||||
(set_local $end (i32.const 0))
|
||||
(set_local $buffer (i32.const 0))
|
||||
(loop $label$1
|
||||
(set_local $buffer
|
||||
(i32.or
|
||||
(i32.shl
|
||||
(i32.load8_s (get_local $begin))
|
||||
(get_local $end)
|
||||
)
|
||||
(get_local $buffer)
|
||||
)
|
||||
)
|
||||
(set_local $begin
|
||||
(i32.add (get_local $begin) (i32.const 1))
|
||||
)
|
||||
(set_local $end
|
||||
(i32.add (get_local $end) (i32.const 8))
|
||||
)
|
||||
(br_if $label$1
|
||||
(tee_local $3
|
||||
(i32.add (get_local $3) (i32.const -1))
|
||||
)
|
||||
)
|
||||
)
|
||||
(return (get_local $buffer))
|
||||
)
|
||||
(i32.const 0)
|
||||
)
|
||||
|
||||
;; int invoke(const char *buffer, int size) {
|
||||
;; if(size != 8) {
|
||||
;; return 0;
|
||||
;; }
|
||||
;;
|
||||
;; const int a = extractInt(buffer, 0, 4);
|
||||
;; const int b = extractInt(buffer, 4, 8);
|
||||
;;
|
||||
;; return a + b;
|
||||
;; }
|
||||
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.ne (get_local $size) (i32.const 8))
|
||||
)
|
||||
(set_local $2
|
||||
(i32.add
|
||||
(call $extractInt
|
||||
(get_local $buffer)
|
||||
(i32.const 0)
|
||||
(i32.const 4)
|
||||
)
|
||||
(call $extractInt
|
||||
(get_local $buffer)
|
||||
(i32.const 4)
|
||||
(i32.const 8)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call $putIntResult (get_local $2))
|
||||
)
|
||||
|
||||
;; int putIntResult(int result) {
|
||||
;; const int address = 1024*1024;
|
||||
;;
|
||||
;; globalBuffer[address] = 0;
|
||||
;; globalBuffer[address + 1] = 0;
|
||||
;; globalBuffer[address + 2] = 0;
|
||||
;; globalBuffer[address + 3] = 4;
|
||||
;;
|
||||
;; for(int i = 0; i < 4; ++i) {
|
||||
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
|
||||
;; }
|
||||
;;
|
||||
;; return address;
|
||||
;; }
|
||||
(func $putIntResult (param $result i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(set_local $2 (i32.const 0))
|
||||
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
|
||||
(set_local $1 (i32.const 1048596))
|
||||
(loop $label$0
|
||||
(i32.store8
|
||||
(get_local $1)
|
||||
(i32.shr_u (get_local $result) (get_local $2))
|
||||
)
|
||||
(set_local $1
|
||||
(i32.add (get_local $1) (i32.const 1))
|
||||
)
|
||||
(br_if $label$0
|
||||
(i32.ne
|
||||
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
|
||||
(i32.const 32)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1048592)
|
||||
)
|
||||
)
|
36
vm/src/test/scala/fluence/vm/TestUtils.scala
Normal file
36
vm/src/test/scala/fluence/vm/TestUtils.scala
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.IO
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object TestUtils {
|
||||
|
||||
implicit class EitherTValueReader[E, V](origin: EitherT[IO, E, V]) {
|
||||
|
||||
def success(timeout: Duration = 3.seconds): V =
|
||||
origin.value.unsafeRunTimed(timeout).get.right.get
|
||||
|
||||
def failed(timeout: Duration = 3.seconds): E =
|
||||
origin.value.unsafeRunTimed(timeout).get.left.get
|
||||
}
|
||||
|
||||
}
|
107
vm/src/test/scala/fluence/vm/WasmVmSpec.scala
Normal file
107
vm/src/test/scala/fluence/vm/WasmVmSpec.scala
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import cats.data.{EitherT, NonEmptyList}
|
||||
import cats.effect.{IO, Timer}
|
||||
import fluence.vm.TestUtils._
|
||||
import fluence.vm.error.InitializationError
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.language.implicitConversions
|
||||
|
||||
class WasmVmSpec extends WordSpec with Matchers {
|
||||
|
||||
implicit def error[E](either: EitherT[IO, E, _]): E = either.value.unsafeRunSync().left.get
|
||||
|
||||
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
|
||||
|
||||
"apply" should {
|
||||
|
||||
"raise error" when {
|
||||
|
||||
"config error" in {
|
||||
val res = for {
|
||||
vm <- WasmVm[IO](NonEmptyList.one("unknown file"), "wrong config namespace")
|
||||
} yield vm
|
||||
|
||||
val error = res.failed()
|
||||
error shouldBe a[InitializationError]
|
||||
error.getMessage should startWith("Unable to parse the virtual machine config")
|
||||
}
|
||||
|
||||
"file not found" in {
|
||||
val res = for {
|
||||
vm <- WasmVm[IO](NonEmptyList.one("unknown file"))
|
||||
} yield vm
|
||||
|
||||
val error = res.failed()
|
||||
error shouldBe a[InitializationError]
|
||||
error.getMessage should startWith("IOError: No such file or directory (os error 2)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"initialize Vm success" when {
|
||||
"one module without name is provided" ignore {
|
||||
val sumFile = getClass.getResource("/wast/sum.wast").getPath
|
||||
|
||||
WasmVm[IO](NonEmptyList.one(sumFile)).success()
|
||||
}
|
||||
|
||||
"one module with name is provided" ignore {
|
||||
// Mul modules have name
|
||||
val mulFile = getClass.getResource("/wast/mul.wast").getPath
|
||||
|
||||
WasmVm[IO](NonEmptyList.one(mulFile)).success()
|
||||
}
|
||||
|
||||
"two modules with different module names are provided" ignore {
|
||||
val sumFile = getClass.getResource("/wast/sum.wast").getPath
|
||||
val mulFile = getClass.getResource("/wast/mul.wast").getPath
|
||||
|
||||
WasmVm[IO](NonEmptyList.of(mulFile, sumFile)).success()
|
||||
}
|
||||
|
||||
"two modules with functions with the same names are provided" ignore {
|
||||
// module without name and with some functions with the same name ("allocate", "deallocate", "invoke", ...)
|
||||
val sum1File = getClass.getResource("/wast/counter.wast").getPath
|
||||
// module with name "Sum" and with some functions with the same name ("allocate", "deallocate", "invoke", ...)
|
||||
val sum2File = getClass.getResource("/wast/mul.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm <- WasmVm[IO](NonEmptyList.of(sum1File, sum2File))
|
||||
} yield vm
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"initialize Vm failed" when {
|
||||
"two main modules provided" ignore {
|
||||
// these modules both don't contain a name section
|
||||
val sumFile = getClass.getResource("/wast/sum.wast").getPath
|
||||
val mulFile = getClass.getResource("/wast/bad-allocation-function-i64.wast").getPath
|
||||
|
||||
WasmVm[IO](NonEmptyList.of(mulFile, sumFile)).failed()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
293
vm/src/test/scala/fluence/vm/WasmerWasmVmSpec.scala
Normal file
293
vm/src/test/scala/fluence/vm/WasmerWasmVmSpec.scala
Normal file
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2019 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Adapt tests for Wasmer
|
||||
|
||||
package fluence.vm
|
||||
|
||||
import java.nio.{ByteBuffer, ByteOrder}
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.{IO, Timer}
|
||||
import fluence.vm.TestUtils._
|
||||
import fluence.vm.error.{InitializationError, InvocationError}
|
||||
import org.scalatest.{Assertion, Matchers, WordSpec}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.language.{higherKinds, implicitConversions}
|
||||
|
||||
class WasmerWasmVmSpec extends WordSpec with Matchers {
|
||||
|
||||
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
|
||||
|
||||
/**
|
||||
* By element comparision of arrays.
|
||||
*/
|
||||
private def compareArrays(first: Array[Byte], second: Array[Byte]): Assertion =
|
||||
first.deep shouldBe second.deep
|
||||
|
||||
/**
|
||||
* Converts ints to byte array by supplied byte order.
|
||||
*
|
||||
* @param ints array of int
|
||||
* @param byteOrder byte order that used for int converting
|
||||
*/
|
||||
private def intsToBytes(
|
||||
ints: List[Int],
|
||||
byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN
|
||||
): ByteBuffer = {
|
||||
val intBytesSize = 4
|
||||
val converter = ByteBuffer.allocate(intBytesSize * ints.length)
|
||||
|
||||
converter.order(byteOrder)
|
||||
ints.foreach(converter.putInt)
|
||||
converter.flip()
|
||||
converter
|
||||
}
|
||||
|
||||
"invoke" should {
|
||||
"raise error" when {
|
||||
|
||||
"trying to invoke when a module doesn't have one" ignore {
|
||||
val noInvokeTestFile = getClass.getResource("/wast/no-invoke.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(noInvokeTestFile))
|
||||
result ← vm.invoke[IO]().toVmError
|
||||
} yield result
|
||||
val error = res.failed()
|
||||
error shouldBe a[InitializationError]
|
||||
error.getMessage should startWith("The main module must have functions with names")
|
||||
}
|
||||
|
||||
"trying to use Wasm memory when getMemory function isn't defined" ignore {
|
||||
val noGetMemoryTestFile = getClass.getResource("/wast/no-getMemory.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(noGetMemoryTestFile))
|
||||
_ ← vm.invoke[IO]("test".getBytes())
|
||||
state ← vm.computeVmState[IO].toVmError
|
||||
} yield state
|
||||
|
||||
val error = res.failed()
|
||||
error.getMessage should
|
||||
startWith("Unable to initialize module=null")
|
||||
error shouldBe a[InitializationError]
|
||||
}
|
||||
|
||||
"wasm code falls into the trap" ignore {
|
||||
val sumTestFile = getClass.getResource("/wast/sum-with-trap.wast").getPath
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(sumTestFile))
|
||||
result ← vm.invoke[IO](fnArgument = intsToBytes(100 :: 13 :: Nil).array()).toVmError // Integer overflow
|
||||
} yield result
|
||||
val error = res.failed()
|
||||
error shouldBe a[InvocationError]
|
||||
error.getMessage should startWith("Function invoke with args:")
|
||||
error.getMessage should include("was failed")
|
||||
}
|
||||
|
||||
"Wasm allocate function returns an incorrect i64 value" ignore {
|
||||
val badAllocationFunctionFile = getClass.getResource("/wast/bad-allocation-function-i64.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(badAllocationFunctionFile))
|
||||
_ ← vm.invoke[IO]("test".getBytes())
|
||||
state ← vm.computeVmState[IO].toVmError
|
||||
} yield state
|
||||
|
||||
val error = res.failed()
|
||||
error.getMessage shouldBe "Writing to -1 failed"
|
||||
error shouldBe a[InvocationError]
|
||||
}
|
||||
|
||||
"Wasm allocate function returns an incorrect f64 value" ignore {
|
||||
val badAllocationFunctionFile = getClass.getResource("/wast/bad-allocation-function-f64.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(badAllocationFunctionFile))
|
||||
result ← vm.invoke[IO]("test".getBytes())
|
||||
state ← vm.computeVmState[IO].toVmError
|
||||
} yield state
|
||||
|
||||
val error = res.failed()
|
||||
error.getMessage shouldBe "Writing to 200000000 failed"
|
||||
error shouldBe a[InvocationError]
|
||||
}
|
||||
|
||||
"trying to extract array with incorrect size from Wasm memory" ignore {
|
||||
val incorrectArrayReturningTestFile = getClass.getResource("/wast/incorrect-array-returning.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(incorrectArrayReturningTestFile))
|
||||
result ← vm.invoke[IO]().toVmError
|
||||
} yield result
|
||||
|
||||
val error = res.failed()
|
||||
error shouldBe a[InvocationError]
|
||||
error.getMessage shouldBe "Reading from offset=1048596 16777215 bytes failed"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
"invokes function success" when {
|
||||
"run sum.wast" ignore {
|
||||
val sumTestFile = getClass.getResource("/wast/sum.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(sumTestFile))
|
||||
result ← vm.invoke[IO](intsToBytes(100 :: 17 :: Nil).array()).toVmError
|
||||
} yield {
|
||||
compareArrays(result.output, Array[Byte](117, 0, 0, 0))
|
||||
}
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
"run counter.wast" ignore {
|
||||
val counterTestFile = getClass.getResource("/wast/counter.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(counterTestFile))
|
||||
get1 ← vm.invoke[IO]() // 0 -> 1; read 1
|
||||
get2 ← vm.invoke[IO]() // 1 -> 2; read 2
|
||||
get3 ← vm.invoke[IO]().toVmError // 2 -> 3; read 3
|
||||
} yield {
|
||||
compareArrays(get1.output, Array[Byte](1, 0, 0, 0))
|
||||
compareArrays(get2.output, Array[Byte](2, 0, 0, 0))
|
||||
compareArrays(get3.output, Array[Byte](3, 0, 0, 0))
|
||||
}
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
"run simple test with array passsing" ignore {
|
||||
val simpleStringPassingTestFile = getClass.getResource("/wast/simple-string-passing.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(simpleStringPassingTestFile))
|
||||
value1 ← vm.invoke[IO]("test_argument".getBytes())
|
||||
value2 ← vm.invoke[IO]("XX".getBytes())
|
||||
value3 ← vm.invoke[IO]("XXX".getBytes())
|
||||
value4 ← vm.invoke[IO]("".getBytes()) // empty string
|
||||
value5 ← vm.invoke[IO]("\"".getBytes()).toVmError // " string
|
||||
} yield {
|
||||
compareArrays(value1.output, Array[Byte](90, 0, 0, 0))
|
||||
compareArrays(value2.output, Array[Byte](0, 0, 0, 0))
|
||||
compareArrays(value3.output, Array[Byte]('X'.toByte, 0, 0, 0))
|
||||
compareArrays(value4.output, Array[Byte](0, 0, 0, 0)) // this Wasm example returns 0 on empty strings
|
||||
compareArrays(value5.output, Array[Byte]('"'.toByte, 0, 0, 0))
|
||||
}
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
"run simple test with array returning" ignore {
|
||||
val simpleArrayPassingTestFile = getClass.getResource("/wast/simple-array-returning.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(simpleArrayPassingTestFile))
|
||||
value1 ← vm.invoke[IO]()
|
||||
_ ← vm.computeVmState[IO].toVmError
|
||||
} yield {
|
||||
val stringValue = new String(value1.output)
|
||||
stringValue shouldBe "Hello from Fluence Labs!"
|
||||
}
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
"run simple test with array mutation" ignore {
|
||||
val simpleArrayMutationTestFile = getClass.getResource("/wast/simple-array-mutation.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(simpleArrayMutationTestFile))
|
||||
value1 ← vm.invoke[IO]("AAAAAAA".getBytes())
|
||||
state ← vm.computeVmState[IO].toVmError
|
||||
} yield {
|
||||
val stringValue = new String(value1.output)
|
||||
stringValue shouldBe "BBBBBBB"
|
||||
}
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"getVmState" should {
|
||||
"returns state" when {
|
||||
"there is one module with memory present" ignore {
|
||||
// the code in 'counter.wast' uses 'memory', instance for this module created with 'memory' field
|
||||
val counterTestFile = getClass.getResource("/wast/counter.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.one(counterTestFile))
|
||||
get1 ← vm.invoke[IO]() // 0 -> 1; return 1
|
||||
state1 ← vm.computeVmState[IO]
|
||||
get2 ← vm.invoke[IO]() // 1 -> 2; return 2
|
||||
|
||||
get3 ← vm.invoke[IO]() // 2 -> 3; return 3
|
||||
state2 ← vm.computeVmState[IO]
|
||||
get4 ← vm.invoke[IO]().toVmError // 3 -> 4; return 4
|
||||
} yield {
|
||||
compareArrays(get1.output, Array[Byte](1, 0, 0, 0))
|
||||
compareArrays(get2.output, Array[Byte](2, 0, 0, 0))
|
||||
compareArrays(get3.output, Array[Byte](3, 0, 0, 0))
|
||||
compareArrays(get4.output, Array[Byte](4, 0, 0, 0))
|
||||
|
||||
state1.size shouldBe 32
|
||||
state2.size shouldBe 32
|
||||
state1 should not be state2
|
||||
}
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
"there are several modules present" ignore {
|
||||
val counterTestFile = getClass.getResource("/wast/counter.wast").getPath
|
||||
val counterCopyTestFile = getClass.getResource("/wast/counter-copy.wast").getPath
|
||||
val mulTestFile = getClass.getResource("/wast/mul.wast").getPath
|
||||
|
||||
val res = for {
|
||||
vm ← WasmVm[IO](NonEmptyList.of(counterTestFile, counterCopyTestFile, mulTestFile))
|
||||
|
||||
get1 ← vm.invoke[IO]() // 0 -> 1; read 1
|
||||
_ ← vm.invoke[IO]() // 1 -> 2; read 2
|
||||
|
||||
state1 ← vm.computeVmState[IO]
|
||||
|
||||
_ ← vm.invoke[IO]() // 2 -> 3
|
||||
get2 ← vm.invoke[IO]() // 3 -> 4
|
||||
|
||||
state2 ← vm.computeVmState[IO].toVmError
|
||||
|
||||
} yield {
|
||||
compareArrays(get1.output, Array[Byte](1, 0, 0, 0))
|
||||
compareArrays(get2.output, Array[Byte](4, 0, 0, 0))
|
||||
|
||||
state1.size shouldBe 32
|
||||
state2.size shouldBe 32
|
||||
state1 should not be state2
|
||||
}
|
||||
|
||||
res.success()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user