diff --git a/docs/org/SUMMARY.org b/docs/org/SUMMARY.org index c1d7a6538e..078352ea7c 100755 --- a/docs/org/SUMMARY.org +++ b/docs/org/SUMMARY.org @@ -6,6 +6,8 @@ - [[./getting-started/quick-start.md][Quick start]] - [[./getting-started/dependencies.md][Installing dependencies]] - [[./examples/README.md][Juvix Examples]] +- [[./tutorials/README.md][Tutorials]] + - [[./tutorials/nodejs-interop.md][NodeJS Interop]] - [[./language-reference/README.md][Language reference]] - [[./language-reference/comments.md][Comments]] diff --git a/docs/org/tutorials/README.org b/docs/org/tutorials/README.org new file mode 100644 index 0000000000..62dd0deb50 --- /dev/null +++ b/docs/org/tutorials/README.org @@ -0,0 +1 @@ + - [[./nodejs-interop.md][NodeJS Interop]] diff --git a/docs/org/tutorials/nodejs-interop.org b/docs/org/tutorials/nodejs-interop.org new file mode 100644 index 0000000000..12ec54c92c --- /dev/null +++ b/docs/org/tutorials/nodejs-interop.org @@ -0,0 +1,108 @@ +* NodeJS Interop + +A Juvix module can be compiled to a Wasm module. When a Wasm module is +instantiated by a host, functions from the host can be injected into a Wasm +module and functions from the Wasm module can be called by the host. + +In this tutorial you will see how to call host functions in Juvix and call Juvix +functions from the host using the Wasm mechanism. + +** The Juvix module + +The following Juvix module has two functions. + +The function =hostDisplayString= is an =axiom= with no corresponding =compile= +block that implements it. We will inject an implementation for this function +when we instantiate the module from NodeJS. + +The function =juvixRender= is a normal Juvix function. We will call this from +NodeJS. + +#+begin_src +-- NodeJsInterop.juvix +module NodeJsInterop; + +open import Stdlib.Prelude; + +axiom hostDisplayString : String → IO; + +juvixRender : IO; +juvixRender ≔ hostDisplayString "Hello World from Juvix!"; + +end; +#+end_src + +** Compiling the Juvix module + +The Juvix module can be compiled using the following command: + +#+begin_src +juvix compile -r standalone NodeJsInterop.juvix +#+end_src + +This will create a file containing a Wasm module called =NodeJsInterop.wasm=. + +** The NodeJS module + +The following NodeJS module demonstrates both calling a Juvix function from +NodeJS and injecting a NodeJS function into a Juvix module. + +The NodeJS function =hostDisplayString= is passed to the Wasm module +=NodeJSInterop.wasm= when it is instantiated. After instantiation the Juvix +function =juvixRender= is called. + +The functions =ptrToCstr= and =cstrlen= are necessary to convert the =char= +pointer passed from Juvix to a JS =String=. + +#+begin_src +-- NodeJSInterop.js +const fs = require('fs'); +let wasmModule = null; + +function cstrlen(mem, ptr) { + let len = 0; + while (mem[ptr] != 0) { + len++; + ptr++; + } + return len; +} + +function ptrToCstr(ptr) { + const wasmMemory = wasmModule.instance.exports.memory.buffer; + const mem = new Uint8Array(wasmMemory); + const len = cstrlen(mem, ptr); + const bytes = new Uint8Array(wasmMemory, ptr, len); + return new TextDecoder().decode(bytes); +} + +function hostDisplayString(strPtr) { + const text = ptrToCstr(strPtr); + console.log(text); +} + +const wasmBuffer = fs.readFileSync("NodeJsInterop.wasm"); +WebAssembly.instantiate(wasmBuffer, { + env: { + hostDisplayString, + } +}).then((w) => { + wasmModule = w; + wasmModule.instance.exports.juvixRender(); +}); +#+end_src + +** Running the Wasm module + +Now you should have the files =NodeJsInterop.wasm= and =NodeJsInterop.js= in the +same directory. Run the following command to execute the module: + +#+begin_src +node NodeJsInterop.js +#+end_src + +You should see the following output: + +#+begin_src +Hello World from Juvix! +#+end_src diff --git a/src/Juvix/Syntax/MiniC/Language.hs b/src/Juvix/Syntax/MiniC/Language.hs index 4cdd0ef1b5..b21559bb92 100644 --- a/src/Juvix/Syntax/MiniC/Language.hs +++ b/src/Juvix/Syntax/MiniC/Language.hs @@ -82,7 +82,7 @@ data Qualifier -- Attributes -------------------------------------------------------------------------------- -newtype Attribute = ExportName Text +data Attribute = ExportName Text | ImportName Text -------------------------------------------------------------------------------- -- Types diff --git a/src/Juvix/Syntax/MiniC/Serialization.hs b/src/Juvix/Syntax/MiniC/Serialization.hs index 1d4480be95..c4172e2572 100644 --- a/src/Juvix/Syntax/MiniC/Serialization.hs +++ b/src/Juvix/Syntax/MiniC/Serialization.hs @@ -29,7 +29,11 @@ prettyCpp = \case prettyAttribute :: Attribute -> HP.Doc prettyAttribute = \case - ExportName n -> "__attribute__" HP.<> HP.parens (HP.parens ("export_name" HP.<> HP.parens (HP.doubleQuotes (prettyText n)))) + ExportName n -> attr "export_name" n + ImportName n -> attr "import_name" n + where + attr :: Text -> Text -> HP.Doc + attr n v = "__attribute__" HP.<> HP.parens (HP.parens (prettyText n HP.<> HP.parens (HP.doubleQuotes (prettyText v)))) prettyCCode :: CCode -> HP.Doc prettyCCode = \case diff --git a/src/Juvix/Translation/MicroJuvixToMiniC.hs b/src/Juvix/Translation/MicroJuvixToMiniC.hs index 8bbf68b154..cbb99338c8 100644 --- a/src/Juvix/Translation/MicroJuvixToMiniC.hs +++ b/src/Juvix/Translation/MicroJuvixToMiniC.hs @@ -461,9 +461,7 @@ goAxiom a | otherwise = do backend <- runFail (lookupBackends (axiomName ^. Micro.nameId) >>= firstBackendMatch) case backend of - Nothing -> do - sig <- ExternalFuncSig <$> (cFunTypeToFunSig defineName <$> typeToFunType (mkPolyType' (a ^. Micro.axiomType))) - return [sig] + Nothing -> genFunctionDef a Just defineBody -> return [ ExternalMacro @@ -495,6 +493,22 @@ goAxiom a NameId -> Sem r [BackendItem] lookupBackends f = ask >>= failMaybe . fmap (^. Scoper.compileInfoBackendItems) . HashMap.lookup f + genFunctionDef :: + forall r. + Members [Reader Micro.InfoTable, Reader CompileInfoTable] r => + Micro.AxiomDef -> + Sem r [CCode] + genFunctionDef d + | T.all isAlphaNum nameText = (ExternalAttribute (ImportName (axiomName ^. Micro.nameText)) :) <$> sig + | otherwise = sig + where + nameText :: Text + nameText = axiomName ^. Micro.nameText + + sig :: Sem r [CCode] + sig = do + s <- cFunTypeToFunSig defineName <$> typeToFunType (mkPolyType' (d ^. Micro.axiomType)) + return [ExternalFuncSig s] goForeign :: ForeignBlock -> [CCode] goForeign b = case b ^. foreignBackend of diff --git a/test/BackendC/Base.hs b/test/BackendC/Base.hs index 5f41ce28b9..3976d0528d 100644 --- a/test/BackendC/Base.hs +++ b/test/BackendC/Base.hs @@ -18,7 +18,7 @@ clangCompile mkClangArgs cResult execute step = withTempDir ( \dirPath -> do let cOutputFile = dirPath "out.c" - wasmOutputFile = dirPath "out.wasm" + wasmOutputFile = dirPath "Input.wasm" TIO.writeFile cOutputFile (cResult ^. MiniC.resultCCode) step "WASM generation" P.callProcess @@ -40,16 +40,8 @@ wasmClangAssertion WASMInfo {..} mainFile expectedFile step = do expected <- TIO.readFile expectedFile - let execute :: FilePath -> IO Text - execute outputFile = - pack - <$> P.readProcess - "wasmer" - (["run", outputFile, "--invoke", unpack _wasmInfoFunctionName] <> (unpack <$> _wasmInfoFunctionArgs)) - "" - step "Compile C with wasm standalone runtime" - actualStandalone <- clangCompile standaloneArgs p execute step + actualStandalone <- clangCompile standaloneArgs p _wasmInfoActual step step "Compare expected and actual program output" assertEqDiff ("Check: WASM output = " <> expectedFile) actualStandalone expected diff --git a/test/BackendC/Positive.hs b/test/BackendC/Positive.hs index 79baba9664..7dac19169f 100644 --- a/test/BackendC/Positive.hs +++ b/test/BackendC/Positive.hs @@ -2,6 +2,7 @@ module BackendC.Positive where import BackendC.Base import Base +import System.Process qualified as P data PosTest = PosTest { _name :: String, @@ -20,6 +21,29 @@ mainFile = "Input.juvix" expectedFile :: FilePath expectedFile = "expected.golden" +actualCallExport :: Text -> [Text] -> FilePath -> IO Text +actualCallExport funName funArgs outputFile = + pack + <$> P.readProcess + "wasmer" + (["run", outputFile, "--invoke", unpack funName] <> (unpack <$> funArgs)) + "" + +actualCallNode :: FilePath -> FilePath -> IO Text +actualCallNode jsFile outputFile = do + assertCmdExists "node" + let outputDir = takeDirectory outputFile + outputJsFile = outputDir jsFile + copyFile jsFile outputJsFile + withCurrentDirectory + outputDir + ( pack + <$> P.readProcess + "node" + [outputJsFile] + "" + ) + testDescr :: PosTest -> TestDescr testDescr PosTest {..} = let tRoot = root _relDir @@ -55,6 +79,7 @@ tests = PosTest "Builtin types and functions" "Builtins" (WASI StdlibExclude), PosTest "Import from embedded standard library" "StdlibImport" (WASI StdlibInclude), PosTest "Axiom without a compile block" "AxiomNoCompile" (WASI StdlibInclude), - PosTest "Invoke a function using exported name" "ExportName" (WASM (WASMInfo "fun" [])), - PosTest "Invoke a function using exported name with args" "ExportNameArgs" (WASM (WASMInfo "fun" ["0"])) + PosTest "Invoke a function using exported name" "ExportName" (WASM (WASMInfo (actualCallExport "fun" []))), + PosTest "Invoke a function using exported name with args" "ExportNameArgs" (WASM (WASMInfo (actualCallExport "fun" ["0"]))), + PosTest "Invoke an imported function in Juvix and exported function in JS" "ImportExportName" (WASM (WASMInfo (actualCallNode "input.js"))) ] diff --git a/test/Base.hs b/test/Base.hs index 387314b8bc..3e2ca03922 100644 --- a/test/Base.hs +++ b/test/Base.hs @@ -26,9 +26,8 @@ data TestDescr = TestDescr _testAssertion :: AssertionDescr } -data WASMInfo = WASMInfo - { _wasmInfoFunctionName :: Text, - _wasmInfoFunctionArgs :: [Text] +newtype WASMInfo = WASMInfo + { _wasmInfoActual :: FilePath -> IO Text } makeLenses ''TestDescr diff --git a/tests/positive/MiniC/ImportExportName/Input.juvix b/tests/positive/MiniC/ImportExportName/Input.juvix new file mode 100644 index 0000000000..11707af409 --- /dev/null +++ b/tests/positive/MiniC/ImportExportName/Input.juvix @@ -0,0 +1,10 @@ +module Input; + +open import Stdlib.Prelude; + +axiom hostDisplayString : String → IO; + +juvixRender : IO; +juvixRender ≔ hostDisplayString "Hello World from Juvix!"; + +end; diff --git a/tests/positive/MiniC/ImportExportName/expected.golden b/tests/positive/MiniC/ImportExportName/expected.golden new file mode 100644 index 0000000000..66283ea681 --- /dev/null +++ b/tests/positive/MiniC/ImportExportName/expected.golden @@ -0,0 +1 @@ +Hello World from Juvix! diff --git a/tests/positive/MiniC/ImportExportName/input.js b/tests/positive/MiniC/ImportExportName/input.js new file mode 100644 index 0000000000..f0c3157fd0 --- /dev/null +++ b/tests/positive/MiniC/ImportExportName/input.js @@ -0,0 +1,35 @@ +const fs = require('fs'); + +let wasmModule = null; + +function cstrlen(mem, ptr) { + let len = 0; + while (mem[ptr] != 0) { + len++; + ptr++; + } + return len; +} + +function ptrToCstr(ptr) { + const wasmMemory = wasmModule.instance.exports.memory.buffer; + const mem = new Uint8Array(wasmMemory); + const len = cstrlen(mem, ptr); + const bytes = new Uint8Array(wasmMemory, ptr, len); + return new TextDecoder().decode(bytes); +} + +function hostDisplayString(strPtr) { + const text = ptrToCstr(strPtr); + console.log(text); +} + +const wasmBuffer = fs.readFileSync("Input.wasm"); +WebAssembly.instantiate(wasmBuffer, { + env: { + hostDisplayString, + } +}).then((w) => { + wasmModule = w; + wasmModule.instance.exports.juvixRender(); +}); diff --git a/tests/positive/MiniC/ImportExportName/juvix.yaml b/tests/positive/MiniC/ImportExportName/juvix.yaml new file mode 100644 index 0000000000..e69de29bb2