Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chasm cannot run wasm generated from Java code via TeaVM #81

Closed
mipastgt opened this issue Jan 28, 2025 · 22 comments
Closed

Chasm cannot run wasm generated from Java code via TeaVM #81

mipastgt opened this issue Jan 28, 2025 · 22 comments

Comments

@mipastgt
Copy link

mipastgt commented Jan 28, 2025

I wonder why I am not able to even get a primitive Java function, compiled via TeaVM to Wasm,
running on Chasm, whereas the same tests work without problems for an equivalent C function compiled via Emscripten.

I have asked the same but more detailed question on the TeaVM issue tracker and got several answers from the author of TeaVM which might explain this. But I still wanted to forward this to you too in case there is also a problem with Chasm that you might want to look into.

One problem, for example, is the fact that you don't get the name of missing imports.

konsoletyper/teavm#1003

@CharlieTap
Copy link
Owner

Hey @mipastgt

I took a quick a look and can see the module depends on these imports:

 (import "teavm" "takeStackTrace" (func (;0;) (type 24)))
  (import "teavm" "decorateException" (func (;1;) (type 40)))
  (import "teavmJso" "stringFromCharCode" (func (;2;) (type 25)))
  (import "teavmJso" "createClass" (func (;3;) (type 30)))
  (import "teavmJso" "defineFunction" (func (;4;) (type 32)))
  (import "teavmJso" "defineStaticMethod" (func (;5;) (type 33)))
  (import "teavmJso" "emptyString" (func (;6;) (type 39)))
  (import "teavmJso" "concatStrings" (func (;7;) (type 42)))
  (import "teavmJso" "unwrapInt" (func (;8;) (type 43)))
  (import "teavmJso" "wrapInt" (func (;9;) (type 25)))

You would need to provide implementations of these using chasms host functions api, I'd have a chat with the guys working on teavm as they will understand better why these are needed

@mipastgt
Copy link
Author

Yes, that's basically what I learned from the author of TeaVM too.

@CharlieTap
Copy link
Owner

@mipastgt Are we okay to close this? Chasm can actually run wasm code generated by teavm the issue here is that those wasm files have imports which are not being provided.

Tbh I'm surprised the imports are present at all given the simplicity of your code, ideally these imports are only lowered into the wasm binary if the code is dependant on something from teavms runtime.

@konsoletyper
Copy link

Chasm can actually run wasm code generated by teavm the issue here is that those wasm files have imports which are not being provided.

I would say that Chasm does not clearly reports what's wrong. @mipastgt posted the following output:

Failed to instantiate module: ExecutionError(error=MissingImport)
java.lang.IllegalStateException: Failed to instantiate module: ExecutionError(error=MissingImport)
    at io.github.charlietap.chasm.embedding.shapes.ChasmResultKt.expect(ChasmResult.kt:56)
    at de.mpmediasoft.wasm.library.LibraryWasmChasmImpl$Companion.instantiateModule(LibraryWasmChasmImpl.kt:29)
    at de.mpmediasoft.wasm.library.LibraryWasmChasmImpl$Companion.access$instantiateModule(LibraryWasmChasmImpl.kt:12)
    at de.mpmediasoft.wasm.library.LibraryWasmChasmImpl.<clinit>(LibraryWasmChasmImpl.kt:35)

which is not enough to understand what's going on. I'd expect at least function name.

ideally these imports are only lowered into the wasm binary if the code is dependant on something from teavms runtime

Just FYI. Real-world code would use more runtime functions, for example, to represent WeakRefs (which are missing in Wasm GC) or to print something to console, etc. Currently, optimizer is good enough to produce 2-3kb for minimal module, so I simply did not want put extra efforts into making this, say 50 bytes.

Also, as for slightly unusual structure of imports/exports. TeaVM used to produce JS code, so the main goal behind Wasm GC BE was to allow to migrate as seamlessly as possible existing JS-targeted code base into Wasm GC. For this purpose, it was important to interact with JS rather than with other Wasm modules or to behave like Wasm. Never expected someone would use it in browserless runtime.

@CharlieTap
Copy link
Owner

Maybe lets make a new issue for improving the error messaging for function imports as I agree this could be more helpful than it currently is

In terms of running teamvm wasm binaries its not going to currently work with the approach that exports a global, the only way to invoke a function currently is for it to be exported. Chasms embedding api is largely a mirror of the spec suggested in the official spec https://webassembly.github.io/spec/core/appendix/embedding.html

@konsoletyper I appreciate you're targeting browsers and have probably arrived at the current solution for performance reasons, its interesting to me that V8 lets you invoke an unexported function through an extern ref. I imagine you'll get more and more requests around this subject as Java is popular and you currently have a means of transforming it to webassembly. If its possible (I haven't looked into TeaVm), making the function exportable and removing the teavm runtime imports (when not needed) would allow these binaries to run on popular wasm engines

@mipastgt
Copy link
Author

We can close this. Thanks for having a look into it.

@konsoletyper
Copy link

its interesting to me that V8 lets you invoke an unexported function through an extern ref

Not exactly. First, TeaVM-generated Wasm module imports defineFunction. Then, module uses ref.func to take a reference to one of module's functions and then passes this reference to imported defineFunction. Host is expected to define this defineFunction as follows: it should produce a function (in terms of host environment) that calls Wasm function reference, passed to defineFunction. If it was possible in Chasm, then it would be possible to write a simple TeaVM GC runtime for Chasm.

@mipastgt
Copy link
Author

Is this related to the error messages produced by GraalWasm
Only function types are supported in the type section
and Chicory
section size mismatch, unexpected end of section or function, We don't support non func types. Form 0x5E was given but we expected 0x60
which also don't run the TeaVM generated code or is this something else?

@konsoletyper
Copy link

@mipastgt you should ask this question GraalmWasm and Chicory team. But I suppose they simply don't support Wasm GC proposal, that's it.

@mipastgt
Copy link
Author

That's possible because I don't see a clear statement anywhere that they do. For me it is a bit diffult to understand all this because I simply know nothing about the internal workings of Wasm. I just want to use it :-)

@konsoletyper
Copy link

@mipastgt what you need is to:

  1. Use TeaVM classic Wasm BE. See this and find mentions of wasm. Then take a look at this example.
  2. Embed any of runtimes and provide all requested function imports (most of them are simply logging functions used from within GC implementation). Their names should be self-descriptive, but if you have troubles, you can refer to this file or ask me.

And of course, you have to learn WebAssembly a little bit in order to use it. Not entire spec with all instructions and all single letter of spec, but of what Wasm module exists, what is Wasm heap, how Wasm functions interact with outer world, what is embedding, etc.

@CharlieTap
Copy link
Owner

@mipastgt I haven't tried this myself but you might be able to include your java sources in a kotlin multiplatform project and have the kotlin wasm compiler create your binary, this would produce a binary chasm can work with similar to how the rust and c compilers you used previously worked

@konsoletyper
Copy link

@CharlieTap no, this would not work. Kotlin multiplatform does not support Java. TeaVM does support mixed Java + Kotlin projects though. BTW, what is the difficulty in introducing function that takes ReferenceValue.Function and number of parameters and calls this function reference?

@mipastgt
Copy link
Author

This does indeed not work as @konsoletyper pointed out. If that would work I'd be happy and wouldn't have to care about any embeddable Wasm runtimes :-) I could then just compile my Kotlin multiplatform project to native code, e.g., for iOS, or to Wasm for the browser and run it directly on the JVM for desktop and Android.

@konsoletyper
Copy link

@mipastgt with TeaVM you have option to compile to C code and then compile this C to native binary.

@mipastgt
Copy link
Author

mipastgt commented Jan 29, 2025

Yes, actually I have already tried that and considered that as a further option for iOS and other native targets.

@konsoletyper
Copy link

@CharlieTap although I don't know your code base, it took me 15 minutes to write a simple proof-of-concept:

Subject: [PATCH] call function
---
Index: executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt
--- a/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt	(revision 806390c7b5bdbcf356d802b2452cde2ca7e91824)
+++ b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt	(date 1738157567135)
@@ -26,6 +26,7 @@
     }.asReversed()
 
     val functionContext = HostFunctionContext(
+        context,
         context.config,
         store,
         frame.instance,
Index: executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/callFunctionRef.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/callFunctionRef.kt b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/callFunctionRef.kt
new file mode 100644
--- /dev/null	(date 1738158902645)
+++ b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/callFunctionRef.kt	(date 1738158902645)
@@ -0,0 +1,79 @@
+package io.github.charlietap.chasm.executor.invoker
+
+import io.github.charlietap.chasm.executor.invoker.instruction.InstructionBlockExecutor
+import io.github.charlietap.chasm.executor.invoker.instruction.admin.FrameInstructionExecutor
+import io.github.charlietap.chasm.executor.runtime.execution.ExecutionContext
+import io.github.charlietap.chasm.executor.runtime.ext.function
+import io.github.charlietap.chasm.executor.runtime.ext.toExecutionValue
+import io.github.charlietap.chasm.executor.runtime.instance.FunctionInstance
+import io.github.charlietap.chasm.executor.runtime.instance.HostFunctionContext
+import io.github.charlietap.chasm.executor.runtime.stack.ActivationFrame
+import io.github.charlietap.chasm.executor.runtime.stack.ControlStack
+import io.github.charlietap.chasm.executor.runtime.stack.FrameStackDepths
+import io.github.charlietap.chasm.executor.runtime.stack.LabelStackDepths
+import io.github.charlietap.chasm.executor.runtime.value.ExecutionValue
+import io.github.charlietap.chasm.executor.runtime.value.ReferenceValue
+
+fun ExecutionContext.callFunctionRef(
+    function: ReferenceValue.Function,
+    vararg params: ExecutionValue,
+): List<ExecutionValue> {
+    return when (val functionInstance = store.function(function.address)) {
+        is FunctionInstance.HostFunction -> {
+            val functionContext = HostFunctionContext(
+                this,
+                this.config,
+                store,
+                cstack.peekFrame().instance,
+            )
+            functionContext.instance
+            functionInstance.function.invoke(functionContext, params.toList())
+        }
+        is FunctionInstance.WasmFunction -> {
+            val cstack = cstack
+            val vstack = vstack
+            val params = functionInstance.functionType.params.types.size
+            val results = functionInstance.functionType.results.types.size
+
+            val valuesDepth = vstack.depth() - params
+            vstack.push(functionInstance.function.locals)
+
+            val depths = FrameStackDepths(
+                handlers = cstack.handlersDepth(),
+                instructions = cstack.instructionsDepth(),
+                labels = cstack.labelsDepth(),
+                values = valuesDepth,
+            )
+            val frame = ActivationFrame(
+                arity = results,
+                depths = depths,
+                instance = functionInstance.module,
+                previousFramePointer = vstack.framePointer,
+            )
+
+            cstack.push(frame)
+            cstack.push { FrameInstructionExecutor(this) }
+
+            val labelDepths = LabelStackDepths(
+                instructions = cstack.instructionsDepth(),
+                labels = cstack.labelsDepth(),
+                values = vstack.depth(),
+            )
+
+            val label = ControlStack.Entry.Label(
+                arity = results,
+                depths = labelDepths,
+                continuation = null,
+            )
+
+            vstack.framePointer = valuesDepth
+            InstructionBlockExecutor(cstack, label, functionInstance.function.body.instructions, null)
+            List(functionInstance.functionType.results.types.size) { vstack.pop() }
+                .asReversed()
+                .zip(functionInstance.functionType.results.types)
+                .map { (encodedValue, type) ->
+                    encodedValue.toExecutionValue(type)
+                }
+        }
+    }
+}
Index: executor/runtime-internal/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/instance/HostFunctionContext.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/executor/runtime-internal/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/instance/HostFunctionContext.kt b/executor/runtime-internal/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/instance/HostFunctionContext.kt
--- a/executor/runtime-internal/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/instance/HostFunctionContext.kt	(revision 806390c7b5bdbcf356d802b2452cde2ca7e91824)
+++ b/executor/runtime-internal/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/instance/HostFunctionContext.kt	(date 1738157567126)
@@ -1,9 +1,11 @@
 package io.github.charlietap.chasm.executor.runtime.instance
 
 import io.github.charlietap.chasm.config.RuntimeConfig
+import io.github.charlietap.chasm.executor.runtime.execution.ExecutionContext
 import io.github.charlietap.chasm.executor.runtime.store.Store
 
 data class HostFunctionContext(
+    internal val context: ExecutionContext,
     val config: RuntimeConfig,
     val store: Store,
     val instance: ModuleInstance,

@CharlieTap
Copy link
Owner

I can certainly add an api to invoke a function by reference, this is actually a suggestion in the wasm spec:

Image

I'm dubious about allowing any function reference to called however, wasm modules have tons of functions and the majority of which are not intended to be called from the host directly. For this to respect the sandboxing it needs to invoke exported functions only otherwise whats the point of having the concept of exports.

My usual reference for things like this is wasmtime as its maintained by bytecode alliance who specify webassembly, if you try to get a reference to a non exported function it fails:

let instance = linker.instantiate(&mut store, &module)?;
let func = instance.get_typed_func::<i32, i32>(&mut store, "internal_func"); // ❌ Will fail if not exported

Anyway I think the suggestion to create a function that allows invoking (exported) functions by reference is a good one. I'll be doing a release on the weekend so can include it in that, you'll still need to create the teavm runtime functions using chasms host functions api and mark the function as exported to make it work however.

@konsoletyper
Copy link

I'm dubious about allowing any function reference to called however,

It's exactly what function reference proposal was for

wasm modules have tons of functions and the majority of which are not intended to be called from the host directly

Sure, but wasm module won't pass all these tons of functions to host functions. Moreover, spec does not allow to take ref.func to a function that is either not exported or has no corresponding element.

let instance = linker.instantiate(&mut store, &module)?;
let func = instance.get_typed_func::<i32, i32>(&mut store, "internal_func"); // ❌ Will fail if not exported

No-no-no, I did not mean that host should take any function from the module directly. Instead, host provides a function, wasm module imports it, takes function reference with ref.func and passes is to this imported function. So when I embed Chasm and construct a HostedFunction, I want to be able to take parameter, cast it to ReferenceValue.Function and call this function.

@CharlieTap
Copy link
Owner

Clearly we're talking about different things here

Chasm supports functions references, period. Any wasm program you generate with function references will work, but teavm does not generate standalone wasm binaries, it requires a collection of imports to work which is effectively a lightweight runtime. This is like generating a wasi wasm binary

It sounds like you want a way from a host function to call another function by reference, rather than an extension to the embedding api you need an extension to the host functions api. This isn't a problem from a sandboxing perspective as that function is executing as part of the runtime. This again would be a nice addition to the api and would allow host functions to do more than they can currently...

However, what I don't understand, is currently the wasm binaries you produce have no functions exported, so how would this process even start? Even if you get the function ref through the global, the embedding api will not let you invoke a function which is not exported that WOULD break sandboxing. So either you're going to have to export a function, I can't really see why you wouldn't want to do this anyway, its the standard way of interacting with a wasm binary

@konsoletyper
Copy link

However, what I don't understand, is currently the wasm binaries you produce have no functions exported, so how would this process even start

It has globals exported. Wasm module fills these exports on initialization.

Even if you get the function ref through the global, the embedding api will not let you invoke a function which is not exported that WOULD break sandboxing

Embedding API allows to invoke a function ref as soon as embedder's function receives this ref, no matter how we actually got it. Wasm spec allows to take references to functions if they are either exported or have corresponding element in elements section.

So either you're going to have to export a function, I can't really see why you wouldn't want to do this anyway, its the standard way of interacting with a wasm binary

Because what's being exported is not a Wasm function ref (which is opaque object from JS's standpoint), but actual JS function. This function is generated by the function teavmJso.defineFunction, provided by the host. Wasm module would generate set of function references, pass them to defineFunction and put result into exported globals. This is done due to compatibility reasons, namely:

  1. Wasm functions are opaque objects, so you can't get their names, prototypes, constructor, use apply, call methods, etc.
  2. TeaVM exports not only functions, but classes. Guess how? There's also defineClass function, provided by host, which takes function reference (constructor) and returns a JS class. There are also set of host functions to define class methods and properties and so on.
  3. Wasm exception is somewhat peculiar from JS's standpoint. So the function, constructed by defineFunction, wraps call to function ref with call and in catch maps exception to normal JS exception, providing all necessary information.
  4. There's no way to represent vararg functions in Wasm, but defineFunction allows to do that.

And so on. The reason is compatibility. Once a user took their Java/Kotlin/Scala/etc codebase and compiled it into JS, whey would take exactly same codebase, change backend to Wasm GC and get exactly the same result that behaves 100% same as previously did JS code.

@CharlieTap
Copy link
Owner

I'll update the host function api to allow calling functions by reference because it sounds like that will do the trick, it will also be a nice upgrade to that api

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants