This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new cad408b88fb9 CAMEL-25606: camel-wasm - Optional runtime compiler
(#27793)
cad408b88fb9 is described below
commit cad408b88fb96a3df36c495ee031d96cd6bdc7fc
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Oct 14 17:16:42 2026 +0530
CAMEL-25606: camel-wasm - Optional runtime compiler (#27793)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/components/wasm.json | 15 +-
.../apache/camel/catalog/docs/wasm-component.adoc | 52 ++++-
components/camel-wasm/pom.xml | 6 +
.../wasm/WasmConfigurationConfigurer.java | 3 +
.../component/wasm/WasmEndpointConfigurer.java | 3 +
.../component/wasm/WasmEndpointUriFactory.java | 3 +-
.../org/apache/camel/component/wasm/wasm.json | 15 +-
.../camel-wasm/src/main/docs/wasm-component.adoc | 52 ++++-
.../camel/component/wasm/WasmConfiguration.java | 18 ++
.../apache/camel/component/wasm/WasmEndpoint.java | 130 +++++++++++
.../apache/camel/component/wasm/WasmProducer.java | 119 ++--------
.../java/org/apache/camel/wasm/WasiCommand.java | 130 +++++++++++
.../component/wasm/WasmCompiledComponentTest.java | 29 +++
.../camel/component/wasm/WasmComponentTest.java | 160 +++++++++++--
.../org/apache/camel/wasm/WasiCommandTest.java | 254 ++++++++++++++++++++-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 5 +
.../endpoint/dsl/WasmEndpointBuilderFactory.java | 48 ++++
17 files changed, 905 insertions(+), 137 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/wasm.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/wasm.json
index 076279f11820..1766e01ee693 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/wasm.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/wasm.json
@@ -34,12 +34,13 @@
"properties": {
"module": { "index": 0, "kind": "path", "displayName": "Module", "group":
"producer", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The WASI Preview 1 command module to
run, as a resource: by default from the classpath, or with a prefix such as
classpath:, file: or http:" },
"args": { "index": 1, "kind": "parameter", "displayName": "Args", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.wasm.WasmConfiguration",
"configurationField": "configuration", "description": "The command line
arguments passed to the module, separated by whitespace. Use double quotes
around an argument that contains whites [...]
- "environment": { "index": 2, "kind": "parameter", "displayName":
"Environment", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "environment.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Environment variables for the module, [...]
- "environmentHeaders": { "index": 3, "kind": "parameter", "displayName":
"Environment Headers", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "A comma separated allow-list of message
headers whose values are passed to the module as environment v [...]
- "failOnNonZeroExit": { "index": 4, "kind": "parameter", "displayName":
"Fail On Non Zero Exit", "group": "producer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Whether a module that exits with a non-zero
exit code fails the exchange with a WasmExi [...]
- "maxMemoryPages": { "index": 5, "kind": "parameter", "displayName": "Max
Memory Pages", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum size of the memory of the module,
in WebAssembly pages of 64 KiB. Rust and Go programs declare no maximum by
[...]
- "maxOutputSize": { "index": 6, "kind": "parameter", "displayName": "Max
Output Size", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum number of bytes the module may
write to its standard output, and separately to its standard error, which are
bu [...]
- "timeout": { "index": 7, "kind": "parameter", "displayName": "Timeout",
"group": "producer", "label": "", "required": false, "type": "duration",
"javaType": "long", "deprecated": false, "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.wasm.WasmConfiguration",
"configurationField": "configuration", "description": "The maximum time the
module may run for one exchange, in milliseconds. When it is exceeded the
module is interrupted and the exchange f [...]
- "lazyStartProducer": { "index": 8, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "compile": { "index": 2, "kind": "parameter", "displayName": "Compile",
"group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Whether to compile the module to JVM bytecode,
with the runtime compiler of Endive, instead of interpreting it [...]
+ "environment": { "index": 3, "kind": "parameter", "displayName":
"Environment", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "environment.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Environment variables for the module, [...]
+ "environmentHeaders": { "index": 4, "kind": "parameter", "displayName":
"Environment Headers", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "A comma separated allow-list of message
headers whose values are passed to the module as environment v [...]
+ "failOnNonZeroExit": { "index": 5, "kind": "parameter", "displayName":
"Fail On Non Zero Exit", "group": "producer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Whether a module that exits with a non-zero
exit code fails the exchange with a WasmExi [...]
+ "maxMemoryPages": { "index": 6, "kind": "parameter", "displayName": "Max
Memory Pages", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum size of the memory of the module,
in WebAssembly pages of 64 KiB. Rust and Go programs declare no maximum by
[...]
+ "maxOutputSize": { "index": 7, "kind": "parameter", "displayName": "Max
Output Size", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum number of bytes the module may
write to its standard output, and separately to its standard error, which are
bu [...]
+ "timeout": { "index": 8, "kind": "parameter", "displayName": "Timeout",
"group": "producer", "label": "", "required": false, "type": "duration",
"javaType": "long", "deprecated": false, "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.wasm.WasmConfiguration",
"configurationField": "configuration", "description": "The maximum time the
module may run for one exchange, in milliseconds. When it is exceeded the
module is interrupted and the exchange f [...]
+ "lazyStartProducer": { "index": 9, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/wasm-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/wasm-component.adoc
index 27b588693644..a270a7aadc77 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/wasm-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/wasm-component.adoc
@@ -100,7 +100,8 @@ Built with `GOOS=wasip1 GOARCH=wasm go build -o upper.wasm`.
For every exchange the component:
-. creates a new instance of the module (the module is parsed once, when the
producer starts),
+. creates a new instance of the module (the module is loaded once per
endpoint, when its first producer is
+initialized),
. runs its `_start` function with the message body as standard input, the
arguments configured with `args` and
the environment variables configured with `environment` and
`environmentHeaders`,
. sets the `CamelWasmExitCode` header to the exit code of the program: 0 when
`_start` returns, otherwise the code
@@ -215,7 +216,54 @@ body is a stream.
Creating an instance per exchange is cheap for a small module, but the
start-up code of the program itself runs
for every message. Programs with a large runtime, such as standard Go
programs, spend most of each run initializing
-that runtime; TinyGo, Rust and C programs start much faster. Modules are
interpreted.
+that runtime; TinyGo, Rust and C programs start much faster.
+
+By default, a module is interpreted. With `compile=true` the module is
translated to JVM bytecode with the
+https://endive.run/docs/execution/runtime-compiler[runtime compiler] of
Endive, and every exchange runs the compiled
+code on its own instance. Everything else stays the same: the headers, the
errors and the limits, including `timeout`
+(a compiled module that never ends is interrupted like an interpreted one).
+
+[source,java]
+----
+from("direct:in")
+ .to("wasm:classpath:upper.wasm?compile=true");
+----
+
+Compiled code is much faster than the interpreter, most of all for modules
that compute a lot per message. How much
+depends on the module, the message and the machine, so measure with your own
module.
+
+The module is compiled once per endpoint, when the first producer of the
endpoint is initialized:
+
+* for a `to`, that is when the route is started for the first time, or at the
first message when the endpoint has
+`lazyStartProducer=true`. Compiling takes time, which grows with the size of
the module;
+* stopping and starting the route does not load or compile the module again:
the endpoint keeps the compiled module
+for as long as it exists. The generated classes are only released when the
endpoint itself is no longer used, that
+is when it is removed from the Camel context or the context is stopped, not
when the route stops;
+* a route reload in development mode (`camel.main.routesReloadEnabled`) by
default removes all routes and
+endpoints, so the module is read and compiled again;
+* with a dynamic endpoint (`toD`), every different URI is an endpoint of its
own with its own compiled copy of the
+module, so many different URIs with `compile=true` make the JVM metaspace
grow. Prefer a static URI and pass the
+values that change in headers (`environmentHeaders`) or in the body.
+
+Keep the default when:
+
+* start-up time matters more than throughput: compiling is done before the
first message is processed, and
+compiled code is slower until the JVM has optimized it;
+* the application runs as a GraalVM native image: the compiler generates
classes while the application runs, which
+a native image cannot load.
+
+A function of the module that is too large for a JVM method (more than 64 KB
of bytecode) is not compiled: it is
+interpreted, the rest of the module is compiled, and the component logs this
once per endpoint at `WARN`. A module
+that the compiler cannot translate at all fails when the first producer of the
endpoint is initialized, with an
+exception that names the module.
+
+The compiler is in the `run.endive:compiler` artifact, which `camel-wasm`
depends on. If it was excluded from the
+classpath, an endpoint with `compile=true` fails as well, with an exception
that names the missing artifact.
+
+Endive also has a
https://endive.run/docs/execution/build-time-compiler[build-time compiler], the
+`run.endive:endive-compiler-maven-plugin` Maven plugin, which translates a
module to classes when the application is
+built, so that nothing is compiled when it runs. The `wasm` endpoint does not
use such classes: it loads the module
+from its location, and interprets it or compiles it as described above.
== Examples
diff --git a/components/camel-wasm/pom.xml b/components/camel-wasm/pom.xml
index dd3cea44b76f..e897dc023532 100644
--- a/components/camel-wasm/pom.xml
+++ b/components/camel-wasm/pom.xml
@@ -54,6 +54,12 @@
<artifactId>wasi</artifactId>
<version>${endive-version}</version>
</dependency>
+ <!-- used with compile=true only: translates a module to JVM bytecode,
once per endpoint -->
+ <dependency>
+ <groupId>run.endive</groupId>
+ <artifactId>compiler</artifactId>
+ <version>${endive-version}</version>
+ </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
diff --git
a/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmConfigurationConfigurer.java
b/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmConfigurationConfigurer.java
index 0666d1cf8d2f..f64d2b3e3070 100644
---
a/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmConfigurationConfigurer.java
+++
b/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmConfigurationConfigurer.java
@@ -24,6 +24,7 @@ public class WasmConfigurationConfigurer extends
org.apache.camel.support.compon
org.apache.camel.component.wasm.WasmConfiguration target =
(org.apache.camel.component.wasm.WasmConfiguration) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
case "args": target.setArgs(property(camelContext,
java.lang.String.class, value)); return true;
+ case "compile": target.setCompile(property(camelContext,
boolean.class, value)); return true;
case "environment": target.setEnvironment(property(camelContext,
java.util.Map.class, value)); return true;
case "environmentheaders":
case "environmentHeaders":
target.setEnvironmentHeaders(property(camelContext, java.lang.String.class,
value)); return true;
@@ -42,6 +43,7 @@ public class WasmConfigurationConfigurer extends
org.apache.camel.support.compon
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
case "args": return java.lang.String.class;
+ case "compile": return boolean.class;
case "environment": return java.util.Map.class;
case "environmentheaders":
case "environmentHeaders": return java.lang.String.class;
@@ -61,6 +63,7 @@ public class WasmConfigurationConfigurer extends
org.apache.camel.support.compon
org.apache.camel.component.wasm.WasmConfiguration target =
(org.apache.camel.component.wasm.WasmConfiguration) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
case "args": return target.getArgs();
+ case "compile": return target.isCompile();
case "environment": return target.getEnvironment();
case "environmentheaders":
case "environmentHeaders": return target.getEnvironmentHeaders();
diff --git
a/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointConfigurer.java
b/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointConfigurer.java
index 66178fed5e04..fa1862e6a304 100644
---
a/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointConfigurer.java
+++
b/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointConfigurer.java
@@ -24,6 +24,7 @@ public class WasmEndpointConfigurer extends
PropertyConfigurerSupport implements
WasmEndpoint target = (WasmEndpoint) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
case "args": target.getConfiguration().setArgs(property(camelContext,
java.lang.String.class, value)); return true;
+ case "compile":
target.getConfiguration().setCompile(property(camelContext, boolean.class,
value)); return true;
case "environment":
target.getConfiguration().setEnvironment(property(camelContext,
java.util.Map.class, value)); return true;
case "environmentheaders":
case "environmentHeaders":
target.getConfiguration().setEnvironmentHeaders(property(camelContext,
java.lang.String.class, value)); return true;
@@ -44,6 +45,7 @@ public class WasmEndpointConfigurer extends
PropertyConfigurerSupport implements
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
case "args": return java.lang.String.class;
+ case "compile": return boolean.class;
case "environment": return java.util.Map.class;
case "environmentheaders":
case "environmentHeaders": return java.lang.String.class;
@@ -65,6 +67,7 @@ public class WasmEndpointConfigurer extends
PropertyConfigurerSupport implements
WasmEndpoint target = (WasmEndpoint) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
case "args": return target.getConfiguration().getArgs();
+ case "compile": return target.getConfiguration().isCompile();
case "environment": return target.getConfiguration().getEnvironment();
case "environmentheaders":
case "environmentHeaders": return
target.getConfiguration().getEnvironmentHeaders();
diff --git
a/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointUriFactory.java
b/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointUriFactory.java
index e1f028bd1f60..a1e19cf62f1c 100644
---
a/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointUriFactory.java
+++
b/components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointUriFactory.java
@@ -24,8 +24,9 @@ public class WasmEndpointUriFactory extends
org.apache.camel.support.component.E
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(9);
+ Set<String> props = new HashSet<>(10);
props.add("args");
+ props.add("compile");
props.add("environment");
props.add("environmentHeaders");
props.add("failOnNonZeroExit");
diff --git
a/components/camel-wasm/src/generated/resources/META-INF/org/apache/camel/component/wasm/wasm.json
b/components/camel-wasm/src/generated/resources/META-INF/org/apache/camel/component/wasm/wasm.json
index 076279f11820..1766e01ee693 100644
---
a/components/camel-wasm/src/generated/resources/META-INF/org/apache/camel/component/wasm/wasm.json
+++
b/components/camel-wasm/src/generated/resources/META-INF/org/apache/camel/component/wasm/wasm.json
@@ -34,12 +34,13 @@
"properties": {
"module": { "index": 0, "kind": "path", "displayName": "Module", "group":
"producer", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The WASI Preview 1 command module to
run, as a resource: by default from the classpath, or with a prefix such as
classpath:, file: or http:" },
"args": { "index": 1, "kind": "parameter", "displayName": "Args", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.wasm.WasmConfiguration",
"configurationField": "configuration", "description": "The command line
arguments passed to the module, separated by whitespace. Use double quotes
around an argument that contains whites [...]
- "environment": { "index": 2, "kind": "parameter", "displayName":
"Environment", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "environment.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Environment variables for the module, [...]
- "environmentHeaders": { "index": 3, "kind": "parameter", "displayName":
"Environment Headers", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "A comma separated allow-list of message
headers whose values are passed to the module as environment v [...]
- "failOnNonZeroExit": { "index": 4, "kind": "parameter", "displayName":
"Fail On Non Zero Exit", "group": "producer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Whether a module that exits with a non-zero
exit code fails the exchange with a WasmExi [...]
- "maxMemoryPages": { "index": 5, "kind": "parameter", "displayName": "Max
Memory Pages", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum size of the memory of the module,
in WebAssembly pages of 64 KiB. Rust and Go programs declare no maximum by
[...]
- "maxOutputSize": { "index": 6, "kind": "parameter", "displayName": "Max
Output Size", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum number of bytes the module may
write to its standard output, and separately to its standard error, which are
bu [...]
- "timeout": { "index": 7, "kind": "parameter", "displayName": "Timeout",
"group": "producer", "label": "", "required": false, "type": "duration",
"javaType": "long", "deprecated": false, "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.wasm.WasmConfiguration",
"configurationField": "configuration", "description": "The maximum time the
module may run for one exchange, in milliseconds. When it is exceeded the
module is interrupted and the exchange f [...]
- "lazyStartProducer": { "index": 8, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "compile": { "index": 2, "kind": "parameter", "displayName": "Compile",
"group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Whether to compile the module to JVM bytecode,
with the runtime compiler of Endive, instead of interpreting it [...]
+ "environment": { "index": 3, "kind": "parameter", "displayName":
"Environment", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "environment.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Environment variables for the module, [...]
+ "environmentHeaders": { "index": 4, "kind": "parameter", "displayName":
"Environment Headers", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "A comma separated allow-list of message
headers whose values are passed to the module as environment v [...]
+ "failOnNonZeroExit": { "index": 5, "kind": "parameter", "displayName":
"Fail On Non Zero Exit", "group": "producer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "Whether a module that exits with a non-zero
exit code fails the exchange with a WasmExi [...]
+ "maxMemoryPages": { "index": 6, "kind": "parameter", "displayName": "Max
Memory Pages", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum size of the memory of the module,
in WebAssembly pages of 64 KiB. Rust and Go programs declare no maximum by
[...]
+ "maxOutputSize": { "index": 7, "kind": "parameter", "displayName": "Max
Output Size", "group": "producer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.wasm.WasmConfiguration", "configurationField":
"configuration", "description": "The maximum number of bytes the module may
write to its standard output, and separately to its standard error, which are
bu [...]
+ "timeout": { "index": 8, "kind": "parameter", "displayName": "Timeout",
"group": "producer", "label": "", "required": false, "type": "duration",
"javaType": "long", "deprecated": false, "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.wasm.WasmConfiguration",
"configurationField": "configuration", "description": "The maximum time the
module may run for one exchange, in milliseconds. When it is exceeded the
module is interrupted and the exchange f [...]
+ "lazyStartProducer": { "index": 9, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
}
}
diff --git a/components/camel-wasm/src/main/docs/wasm-component.adoc
b/components/camel-wasm/src/main/docs/wasm-component.adoc
index 27b588693644..a270a7aadc77 100644
--- a/components/camel-wasm/src/main/docs/wasm-component.adoc
+++ b/components/camel-wasm/src/main/docs/wasm-component.adoc
@@ -100,7 +100,8 @@ Built with `GOOS=wasip1 GOARCH=wasm go build -o upper.wasm`.
For every exchange the component:
-. creates a new instance of the module (the module is parsed once, when the
producer starts),
+. creates a new instance of the module (the module is loaded once per
endpoint, when its first producer is
+initialized),
. runs its `_start` function with the message body as standard input, the
arguments configured with `args` and
the environment variables configured with `environment` and
`environmentHeaders`,
. sets the `CamelWasmExitCode` header to the exit code of the program: 0 when
`_start` returns, otherwise the code
@@ -215,7 +216,54 @@ body is a stream.
Creating an instance per exchange is cheap for a small module, but the
start-up code of the program itself runs
for every message. Programs with a large runtime, such as standard Go
programs, spend most of each run initializing
-that runtime; TinyGo, Rust and C programs start much faster. Modules are
interpreted.
+that runtime; TinyGo, Rust and C programs start much faster.
+
+By default, a module is interpreted. With `compile=true` the module is
translated to JVM bytecode with the
+https://endive.run/docs/execution/runtime-compiler[runtime compiler] of
Endive, and every exchange runs the compiled
+code on its own instance. Everything else stays the same: the headers, the
errors and the limits, including `timeout`
+(a compiled module that never ends is interrupted like an interpreted one).
+
+[source,java]
+----
+from("direct:in")
+ .to("wasm:classpath:upper.wasm?compile=true");
+----
+
+Compiled code is much faster than the interpreter, most of all for modules
that compute a lot per message. How much
+depends on the module, the message and the machine, so measure with your own
module.
+
+The module is compiled once per endpoint, when the first producer of the
endpoint is initialized:
+
+* for a `to`, that is when the route is started for the first time, or at the
first message when the endpoint has
+`lazyStartProducer=true`. Compiling takes time, which grows with the size of
the module;
+* stopping and starting the route does not load or compile the module again:
the endpoint keeps the compiled module
+for as long as it exists. The generated classes are only released when the
endpoint itself is no longer used, that
+is when it is removed from the Camel context or the context is stopped, not
when the route stops;
+* a route reload in development mode (`camel.main.routesReloadEnabled`) by
default removes all routes and
+endpoints, so the module is read and compiled again;
+* with a dynamic endpoint (`toD`), every different URI is an endpoint of its
own with its own compiled copy of the
+module, so many different URIs with `compile=true` make the JVM metaspace
grow. Prefer a static URI and pass the
+values that change in headers (`environmentHeaders`) or in the body.
+
+Keep the default when:
+
+* start-up time matters more than throughput: compiling is done before the
first message is processed, and
+compiled code is slower until the JVM has optimized it;
+* the application runs as a GraalVM native image: the compiler generates
classes while the application runs, which
+a native image cannot load.
+
+A function of the module that is too large for a JVM method (more than 64 KB
of bytecode) is not compiled: it is
+interpreted, the rest of the module is compiled, and the component logs this
once per endpoint at `WARN`. A module
+that the compiler cannot translate at all fails when the first producer of the
endpoint is initialized, with an
+exception that names the module.
+
+The compiler is in the `run.endive:compiler` artifact, which `camel-wasm`
depends on. If it was excluded from the
+classpath, an endpoint with `compile=true` fails as well, with an exception
that names the missing artifact.
+
+Endive also has a
https://endive.run/docs/execution/build-time-compiler[build-time compiler], the
+`run.endive:endive-compiler-maven-plugin` Maven plugin, which translates a
module to classes when the application is
+built, so that nothing is compiled when it runs. The `wasm` endpoint does not
use such classes: it loads the module
+from its location, and interprets it or compiles it as described above.
== Examples
diff --git
a/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmConfiguration.java
b/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmConfiguration.java
index fc98087ed06f..c45fde736267 100644
---
a/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmConfiguration.java
+++
b/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmConfiguration.java
@@ -30,6 +30,8 @@ public class WasmConfiguration implements Cloneable {
@UriParam
private String args;
+ @UriParam
+ private boolean compile;
@UriParam(prefix = "environment.", multiValue = true)
private Map<String, Object> environment;
@UriParam
@@ -57,6 +59,22 @@ public class WasmConfiguration implements Cloneable {
this.args = args;
}
+ public boolean isCompile() {
+ return compile;
+ }
+
+ /**
+ * Whether to compile the module to JVM bytecode, with the runtime
compiler of Endive, instead of interpreting it.
+ * The module is compiled once per endpoint, when the first producer of
the endpoint is initialized (with the route,
+ * or at the first message with lazyStartProducer), which takes time for a
large module; every exchange then runs
+ * the compiled code, which is much faster than the interpreter. Stopping
and starting the route does not compile
+ * the module again. All other options work the same way. The compiler
generates classes at run time, so this option
+ * cannot be used in a GraalVM native image.
+ */
+ public void setCompile(boolean compile) {
+ this.compile = compile;
+ }
+
public Map<String, Object> getEnvironment() {
return environment;
}
diff --git
a/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmEndpoint.java
b/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmEndpoint.java
index 96393a04c4ce..5e0a663ee21d 100644
---
a/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmEndpoint.java
+++
b/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmEndpoint.java
@@ -16,6 +16,12 @@
*/
package org.apache.camel.component.wasm;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
import org.apache.camel.Category;
import org.apache.camel.Component;
import org.apache.camel.Consumer;
@@ -26,7 +32,11 @@ import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriPath;
import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.wasm.WasiCommand;
import org.apache.camel.wasm.Wasm;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
/**
* Run Wasm programs (WASI Preview 1 command modules) with the message body as
standard input.
@@ -36,6 +46,8 @@ import org.apache.camel.wasm.Wasm;
headersClass = Wasm.Headers.class)
public class WasmEndpoint extends DefaultEndpoint {
+ private static final String DEFAULT_PROGRAM_NAME = "module.wasm";
+
@Metadata(required = true)
@UriPath(description = "The WASI Preview 1 command module to run, as a
resource: by default from the classpath, or"
+ " with a prefix such as classpath:, file: or
http:")
@@ -43,6 +55,9 @@ public class WasmEndpoint extends DefaultEndpoint {
@UriParam
private WasmConfiguration configuration;
+ private final Lock lock = new ReentrantLock();
+ private volatile WasiCommand command;
+
public WasmEndpoint(String endpointUri, Component component, String module,
WasmConfiguration configuration) {
super(endpointUri, component);
@@ -63,6 +78,121 @@ public class WasmEndpoint extends DefaultEndpoint {
return configuration;
}
+ /**
+ * The module of this endpoint as a command. The module is loaded, checked
and, with compile=true, compiled on the
+ * first call, by the first producer that is initialized; all producers of
the endpoint then share the result, which
+ * is immutable, for as long as the endpoint exists. So a route that is
stopped and started again gets a new
+ * producer, but neither loads nor compiles the module again, and the
classes generated for a compiled module can
+ * only be unloaded when the endpoint is no longer reachable (removed from
the context, or the context stopped), not
+ * when a route stops.
+ * <p/>
+ * A failed call leaves nothing behind, so the next call (of the next
producer that is initialized) tries again.
+ */
+ WasiCommand command() throws Exception {
+ WasiCommand answer = command;
+ if (answer == null) {
+ lock.lock();
+ try {
+ answer = command;
+ if (answer == null) {
+ List<String> arguments
+ = arguments(programName(module),
configuration.getArgs());
+ for (String argument : arguments) {
+ checkNoNul("argument " + argument, argument);
+ }
+ WasmModule parsed;
+ try (InputStream is
+ =
ResourceHelper.resolveMandatoryResourceAsInputStream(getCamelContext(),
module)) {
+ parsed = Parser.parse(is);
+ }
+ answer = new WasiCommand(
+ parsed, arguments,
configuration.getMaxMemoryPages(), configuration.isCompile());
+ command = answer;
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+ return answer;
+ }
+
+ static String checkNoNul(String what, String value) {
+ // WASI passes arguments and environment variables as NUL terminated
strings
+ if (value.indexOf('\0') >= 0) {
+ throw new IllegalArgumentException("The " + what + " contains a
NUL character, which a module cannot receive");
+ }
+ return value;
+ }
+
+ /**
+ * The program name the module receives as argument zero: the file name of
the module location, without scheme,
+ * directories, user info or query. The location itself is not passed on:
it can carry credentials (an http location
+ * with user info or a token in the query) and shows host paths to a
module that has no file access.
+ */
+ static String programName(String location) {
+ String name = location;
+ int query = name.indexOf('?');
+ if (query >= 0) {
+ name = name.substring(0, query);
+ }
+ int scheme = name.indexOf(':');
+ if (scheme > 0) {
+ String prefix = name.substring(0, scheme);
+ if ("base64".equals(prefix) || "gzip".equals(prefix)) {
+ // the rest is the module itself, not a name
+ return DEFAULT_PROGRAM_NAME;
+ }
+ name = name.substring(scheme + 1);
+ }
+ if (name.startsWith("//")) {
+ // host, port and user info are not part of the name
+ int path = name.indexOf('/', 2);
+ name = path < 0 ? "" : name.substring(path);
+ }
+ name = name.substring(Math.max(name.lastIndexOf('/'),
name.lastIndexOf('\\')) + 1);
+ return name.isEmpty() ? DEFAULT_PROGRAM_NAME : name;
+ }
+
+ /**
+ * Splits the args option into the argument list of the module, with the
program name first. Arguments are separated
+ * by whitespace; double quotes group an argument that contains whitespace.
+ */
+ static List<String> arguments(String program, String args) {
+ List<String> answer = new ArrayList<>();
+ answer.add(program);
+ if (args == null) {
+ return answer;
+ }
+ StringBuilder current = null;
+ boolean quoted = false;
+ for (int i = 0; i < args.length(); i++) {
+ char c = args.charAt(i);
+ if (c == '"') {
+ quoted = !quoted;
+ if (current == null) {
+ current = new StringBuilder();
+ }
+ } else if (Character.isWhitespace(c) && !quoted) {
+ if (current != null) {
+ answer.add(current.toString());
+ current = null;
+ }
+ } else {
+ if (current == null) {
+ current = new StringBuilder();
+ }
+ current.append(c);
+ }
+ }
+ if (quoted) {
+ throw new IllegalArgumentException("Unbalanced double quote in
args: " + args);
+ }
+ if (current != null) {
+ answer.add(current.toString());
+ }
+ return answer;
+ }
+
@Override
public Producer createProducer() throws Exception {
return new WasmProducer(this);
diff --git
a/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmProducer.java
b/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmProducer.java
index acda97e2730c..c0cc897145cb 100644
---
a/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmProducer.java
+++
b/components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmProducer.java
@@ -42,23 +42,20 @@ import org.apache.camel.ExchangeTimedOutException;
import org.apache.camel.Message;
import org.apache.camel.StreamCache;
import org.apache.camel.support.DefaultProducer;
-import org.apache.camel.support.ResourceHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
import org.apache.camel.wasm.WasiCommand;
import org.apache.camel.wasm.Wasm;
import run.endive.runtime.TrapException;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmModule;
/**
* Runs a WASI Preview 1 command module once per exchange: the message body is
the standard input of the module and its
* standard output becomes the message body.
* <p/>
- * The module is parsed once, when the producer is initialized, and every
exchange runs on a new instance of it, so
- * exchanges never share guest memory or globals and need no lock. The
instance is dropped after its run, whatever the
- * outcome.
+ * The module is loaded (and, with compile=true, compiled) once per endpoint,
when its first producer is initialized,
+ * and every exchange runs on a new instance of it, so exchanges never share
guest memory or globals and need no lock.
+ * The instance is dropped after its run, whatever the outcome.
* <p/>
* Without a timeout the module runs on the calling thread and reads the body
as a stream. With a timeout the module
* runs on a thread of the producer, and the body is first read into memory on
the calling thread: the module then reads
@@ -76,7 +73,6 @@ import run.endive.wasm.WasmModule;
public class WasmProducer extends DefaultProducer {
private static final int MAX_STDERR_IN_MESSAGE = 256;
- private static final String DEFAULT_PROGRAM_NAME = "module.wasm";
private WasiCommand command;
private Map<String, String> environment = Collections.emptyMap();
@@ -103,7 +99,7 @@ public class WasmProducer extends DefaultProducer {
if (conf.getEnvironment() != null) {
for (Map.Entry<String, Object> e :
conf.getEnvironment().entrySet()) {
String value = e.getValue() != null ? e.getValue().toString()
: "";
- env.put(checkName(e.getKey()), checkNoNul("environment
variable " + e.getKey(), value));
+ env.put(checkName(e.getKey()),
WasmEndpoint.checkNoNul("environment variable " + e.getKey(), value));
}
}
List<String> headers = new ArrayList<>();
@@ -117,20 +113,19 @@ public class WasmProducer extends DefaultProducer {
headers.add(name);
}
}
- List<String> arguments = arguments(programName(endpoint.getModule()),
conf.getArgs());
- for (String argument : arguments) {
- checkNoNul("argument " + argument, argument);
- }
-
- WasmModule module;
- try (InputStream is =
ResourceHelper.resolveMandatoryResourceAsInputStream(
- endpoint.getCamelContext(), endpoint.getModule())) {
- module = Parser.parse(is);
- }
this.environment = Collections.unmodifiableMap(env);
this.environmentHeaders = List.copyOf(headers);
- this.command = new WasiCommand(module, arguments,
conf.getMaxMemoryPages());
+ // loaded (and compiled) once per endpoint, not once per producer
+ this.command = endpoint.command();
+ }
+
+ /**
+ * The command of the endpoint that this producer runs; null before the
producer is initialized. Package-private for
+ * tests only.
+ */
+ WasiCommand command() {
+ return command;
}
@Override
@@ -239,9 +234,10 @@ public class WasmProducer extends DefaultProducer {
/**
* Runs the module on the calling thread, or on a thread of the producer
when a timeout is set. On timeout that
- * thread is interrupted: Endive checks the interrupt flag on every call
and every branch, and its WASI sleep
- * (poll_oneoff) ends on an interrupt, so a module stops at its next call
or loop iteration. Interrupting a thread
- * of the producer, not the calling thread, keeps the interrupt flag away
from the threads of the route.
+ * thread is interrupted: Endive checks the interrupt flag on every call
and every branch, in the interpreter and in
+ * compiled code, and its WASI sleep (poll_oneoff) ends on an interrupt,
so a module stops at its next call or loop
+ * iteration. Interrupting a thread of the producer, not the calling
thread, keeps the interrupt flag away from the
+ * threads of the route.
* <p/>
* The threads come from a cached pool without a maximum size. It needs
none: a thread is only taken while an
* exchange waits for it, so there are as many threads as exchanges in
this producer at the same time, plus the
@@ -291,7 +287,7 @@ public class WasmProducer extends DefaultProducer {
for (String name : environmentHeaders) {
String value = message.getHeader(name, String.class);
if (value != null) {
- answer.put(name, checkNoNul("header " + name, value));
+ answer.put(name, WasmEndpoint.checkNoNul("header " + name,
value));
}
}
return answer;
@@ -320,83 +316,6 @@ public class WasmProducer extends DefaultProducer {
return name;
}
- private static String checkNoNul(String what, String value) {
- // WASI passes arguments and environment variables as NUL terminated
strings
- if (value.indexOf('\0') >= 0) {
- throw new IllegalArgumentException("The " + what + " contains a
NUL character, which a module cannot receive");
- }
- return value;
- }
-
- /**
- * The program name the module receives as argument zero: the file name of
the module location, without scheme,
- * directories, user info or query. The location itself is not passed on:
it can carry credentials (an http location
- * with user info or a token in the query) and shows host paths to a
module that has no file access.
- */
- static String programName(String location) {
- String name = location;
- int query = name.indexOf('?');
- if (query >= 0) {
- name = name.substring(0, query);
- }
- int scheme = name.indexOf(':');
- if (scheme > 0) {
- String prefix = name.substring(0, scheme);
- if ("base64".equals(prefix) || "gzip".equals(prefix)) {
- // the rest is the module itself, not a name
- return DEFAULT_PROGRAM_NAME;
- }
- name = name.substring(scheme + 1);
- }
- if (name.startsWith("//")) {
- // host, port and user info are not part of the name
- int path = name.indexOf('/', 2);
- name = path < 0 ? "" : name.substring(path);
- }
- name = name.substring(Math.max(name.lastIndexOf('/'),
name.lastIndexOf('\\')) + 1);
- return name.isEmpty() ? DEFAULT_PROGRAM_NAME : name;
- }
-
- /**
- * Splits the args option into the argument list of the module, with the
program name first. Arguments are separated
- * by whitespace; double quotes group an argument that contains whitespace.
- */
- static List<String> arguments(String program, String args) {
- List<String> answer = new ArrayList<>();
- answer.add(program);
- if (args == null) {
- return answer;
- }
- StringBuilder current = null;
- boolean quoted = false;
- for (int i = 0; i < args.length(); i++) {
- char c = args.charAt(i);
- if (c == '"') {
- quoted = !quoted;
- if (current == null) {
- current = new StringBuilder();
- }
- } else if (Character.isWhitespace(c) && !quoted) {
- if (current != null) {
- answer.add(current.toString());
- current = null;
- }
- } else {
- if (current == null) {
- current = new StringBuilder();
- }
- current.append(c);
- }
- }
- if (quoted) {
- throw new IllegalArgumentException("Unbalanced double quote in
args: " + args);
- }
- if (current != null) {
- answer.add(current.toString());
- }
- return answer;
- }
-
/**
* Thrown by a {@link LimitedOutputStream} that is full. It is not an
IOException, so that it stops the module
* instead of being returned to the module as a failed write.
diff --git
a/components/camel-wasm/src/main/java/org/apache/camel/wasm/WasiCommand.java
b/components/camel-wasm/src/main/java/org/apache/camel/wasm/WasiCommand.java
index 6702d5247c81..4dd723f1c055 100644
--- a/components/camel-wasm/src/main/java/org/apache/camel/wasm/WasiCommand.java
+++ b/components/camel-wasm/src/main/java/org/apache/camel/wasm/WasiCommand.java
@@ -19,19 +19,29 @@ package org.apache.camel.wasm;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import run.endive.compiler.InterpreterFallback;
+import run.endive.compiler.MachineFactoryCompiler;
import run.endive.runtime.ImportValues;
import run.endive.runtime.Instance;
+import run.endive.runtime.Machine;
import run.endive.runtime.Memory;
import run.endive.runtime.TrapException;
import run.endive.wasi.WasiExitException;
import run.endive.wasi.WasiOptions;
import run.endive.wasi.WasiPreview1;
+import run.endive.wasm.WasmEngineException;
import run.endive.wasm.WasmModule;
import run.endive.wasm.types.Export;
import run.endive.wasm.types.ExportSection;
@@ -47,9 +57,15 @@ import run.endive.wasm.types.MemorySection;
* guest memory or globals and need no lock. A run gets exactly the standard
streams, arguments and environment
* variables it is given: no preopened directories (so no files) and no
sockets. The clock is the host's, and
* {@code random_get} is served by a {@link SecureRandom} of the host.
+ * <p/>
+ * The module is interpreted, unless the command is created with {@code
compile} set: then the module is translated to
+ * JVM bytecode once, in the constructor, and every run executes that code on
its own instance. The generated classes
+ * belong to the command: they can be unloaded when the command is no longer
reachable.
*/
public final class WasiCommand {
+ private static final Logger LOG =
LoggerFactory.getLogger(WasiCommand.class);
+
private static final String START = "_start";
private static final String WASI_MODULE = "wasi_snapshot_preview1";
@@ -63,8 +79,13 @@ public final class WasiCommand {
private final WasmModule module;
private final List<String> arguments;
private final MemoryLimits memoryLimits;
+ /** Creates the compiled code of the module for an instance; null when the
module is interpreted. */
+ private final Function<Instance, Machine> machineFactory;
+ private final boolean interpreterFallback;
/**
+ * Creates a command that interprets the module.
+ *
* @param module the parsed module
* @param arguments the program's arguments, the program
name first
* @param maxMemoryPages the maximum number of 64 KiB pages the
module's memory may grow to, or 0 to keep
@@ -73,10 +94,80 @@ public final class WasiCommand {
* than the initial memory the module
declares
*/
public WasiCommand(WasmModule module, List<String> arguments, int
maxMemoryPages) {
+ this(module, arguments, maxMemoryPages, false);
+ }
+
+ /**
+ * @param module the parsed module
+ * @param arguments the program's arguments, the program
name first
+ * @param maxMemoryPages the maximum number of 64 KiB pages the
module's memory may grow to, or 0 to keep
+ * the maximum the module declares
+ * @param compile whether to translate the module to JVM
bytecode now, with the Endive runtime
+ * compiler, instead of interpreting it.
This defines classes at run time, which a
+ * GraalVM native image cannot do. A
function that is too large for a JVM method is
+ * left to the interpreter, which is
logged once, at WARN
+ * @throws IllegalArgumentException if the module is not a WASI Preview 1
command module, or maxMemoryPages is lower
+ * than the initial memory the module
declares
+ * @throws IllegalStateException if compile is set and the Endive
runtime compiler is not on the classpath, or
+ * cannot compile the module. The message
names the module by its program name (the
+ * first argument)
+ */
+ public WasiCommand(WasmModule module, List<String> arguments, int
maxMemoryPages, boolean compile) {
this.module = Objects.requireNonNull(module);
this.arguments = List.copyOf(arguments);
validate(module);
this.memoryLimits = memoryLimits(module, maxMemoryPages);
+
+ Function<Instance, Machine> factory = null;
+ boolean fallback = false;
+ if (compile) {
+ String name = this.arguments.isEmpty() ? "(no name)" :
this.arguments.get(0);
+ try {
+ try {
+ factory = RuntimeCompiler.compile(module, false);
+ } catch (WasmEngineException e) {
+ // Endive could not compile every function. When the
reason is a function that is too large for a
+ // JVM method, the module still works with that function
interpreted, so compile again with the
+ // interpreter as fallback. For any other reason a second
attempt would fail as the first one did.
+ if (!RuntimeCompiler.isFunctionTooLarge(e)) {
+ throw e;
+ }
+ factory = RuntimeCompiler.compile(module, true);
+ fallback = true;
+ LOG.warn("The Wasm module {} is compiled in part only: it
has functions that are too large for a"
+ + " JVM method, which are interpreted",
+ name);
+ LOG.debug("The Endive compiler reported for the Wasm
module {}: {}", name, e.getMessage());
+ }
+ } catch (NoClassDefFoundError e) {
+ throw new IllegalStateException(
+ "compile=true needs the Endive runtime compiler on the
classpath (run.endive:compiler, a dependency"
+ + " of camel-wasm, and its ASM
dependencies), but a class is missing: "
+ + e.getMessage(),
+ e);
+ } catch (RuntimeException e) {
+ throw new IllegalStateException(
+ "The Wasm module " + name + " cannot be compiled to
JVM bytecode (compile=true): " + e.getMessage(),
+ e);
+ }
+ }
+ this.machineFactory = factory;
+ this.interpreterFallback = fallback;
+ }
+
+ /**
+ * Whether the module was compiled to JVM bytecode; otherwise it is
interpreted.
+ */
+ public boolean isCompiled() {
+ return machineFactory != null;
+ }
+
+ /**
+ * Whether the module was compiled with some of its functions left to the
interpreter, because they are too large
+ * for a JVM method. Package-private for tests only.
+ */
+ boolean usesInterpreterFallback() {
+ return interpreterFallback;
}
/**
@@ -139,9 +230,48 @@ public final class WasiCommand {
if (memoryLimits != null) {
builder.withMemoryLimits(memoryLimits);
}
+ if (machineFactory != null) {
+ builder.withMachineFactory(machineFactory);
+ }
return builder.build();
}
+ /**
+ * The only class that refers to the Endive runtime compiler, so that this
command can be loaded and used with the
+ * interpreter when the compiler is not on the classpath.
+ */
+ static final class RuntimeCompiler {
+
+ // ASM's org.objectweb.asm.MethodTooLargeException, compared by name:
the class is a dependency of the compiler
+ private static final String METHOD_TOO_LARGE =
"org.objectweb.asm.MethodTooLargeException";
+
+ /**
+ * Compiles the module. A function that is too large for a JVM method
either fails the compilation, or is left
+ * to the interpreter without a message. Endive's own default
(InterpreterFallback.WARN) is not used: it reports
+ * the fallback on System.err, past the logging of the application.
+ */
+ static Function<Instance, Machine> compile(WasmModule module, boolean
interpretTooLargeFunctions) {
+ return MachineFactoryCompiler.builder(module)
+ .withInterpreterFallback(
+ interpretTooLargeFunctions ?
InterpreterFallback.SILENT : InterpreterFallback.FAIL)
+ .compile();
+ }
+
+ /**
+ * Whether the compilation failed because a function is too large for
a JVM method: Endive reports that with a
+ * WasmEngineException caused by the MethodTooLargeException of ASM.
+ */
+ static boolean isFunctionTooLarge(Throwable failure) {
+ Set<Throwable> seen = Collections.newSetFromMap(new
IdentityHashMap<>());
+ for (Throwable t = failure; t != null && seen.add(t); t =
t.getCause()) {
+ if (METHOD_TOO_LARGE.equals(t.getClass().getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
private static void validate(WasmModule module) {
ExportSection exports = module.exportSection();
boolean hasStart = false;
diff --git
a/components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmCompiledComponentTest.java
b/components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmCompiledComponentTest.java
new file mode 100644
index 000000000000..e70ad0d09f8f
--- /dev/null
+++
b/components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmCompiledComponentTest.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.camel.component.wasm;
+
+/**
+ * Runs every test of {@link WasmComponentTest} with compile=true: results,
headers, limits, timeout and errors are the
+ * same whether a module is interpreted or compiled.
+ */
+class WasmCompiledComponentTest extends WasmComponentTest {
+
+ @Override
+ boolean compile() {
+ return true;
+ }
+}
diff --git
a/components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmComponentTest.java
b/components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmComponentTest.java
index 3474981562cd..b7137dcc87d2 100644
---
a/components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmComponentTest.java
+++
b/components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmComponentTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.wasm;
import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -48,6 +49,8 @@ import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.support.TypeConverterSupport;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.wasm.WasiCommand;
import org.apache.camel.wasm.Wasm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -62,7 +65,8 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
/**
- * Runs small WASI Preview 1 command modules, written in WAT in this class and
compiled when the tests run.
+ * Runs small WASI Preview 1 command modules, written in WAT in this class and
translated to Wasm when the tests run.
+ * The modules are interpreted here; {@link WasmCompiledComponentTest} runs
the same tests with compile=true.
*/
class WasmComponentTest {
@@ -250,34 +254,52 @@ class WasmComponentTest {
(drop (call $fd_write (i32.const 1) (i32.const 0) (i32.const
1) (i32.const 8)))))
""";
+ /** The start of the names of the classes the Endive runtime compiler
generates for a module. */
+ private static final String COMPILED_CLASS = "run.endive.$gen.";
+
@TempDir
- static Path dir;
+ Path dir;
/**
- * Compiles the prelude plus the given functions to a module file and
returns its location for the endpoint URI.
+ * Whether the endpoints of the tests compile their module (the compile
option) or interpret it.
*/
- private static String module(String name, String functions) throws
Exception {
+ boolean compile() {
+ return false;
+ }
+
+ /**
+ * Translates the prelude plus the given functions to a module file and
returns its location for the endpoint URI.
+ */
+ private String module(String name, String functions) throws Exception {
return wat(name, PRELUDE + functions + ")");
}
- private static String wat(String name, String wat) throws Exception {
+ private String wat(String name, String wat) throws Exception {
Path file = dir.resolve(name + ".wasm");
Files.write(file, Wat2Wasm.parse(wat));
return "file:" + file.toAbsolutePath();
}
- private static CamelContext context(String uri) throws Exception {
+ /**
+ * The endpoint URI with the compile option of this class: every route of
the tests uses it.
+ */
+ private String endpoint(String uri) {
+ return uri + (uri.contains("?") ? "&" : "?") + "compile=" + compile();
+ }
+
+ private CamelContext context(String uri) throws Exception {
+ String endpoint = endpoint(uri);
CamelContext cc = new DefaultCamelContext();
cc.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- from("direct:in").routeId("wasm").to(uri);
+ from("direct:in").routeId("wasm").to(endpoint);
}
});
return cc;
}
- private static CamelContext start(String uri) throws Exception {
+ private CamelContext start(String uri) throws Exception {
CamelContext cc = context(uri);
cc.start();
return cc;
@@ -287,14 +309,14 @@ class WasmComponentTest {
* Starts a context that hands a stream body to the endpoint as it is,
instead of as a StreamCache that was already
* read into memory when the exchange entered the route.
*/
- private static CamelContext startWithoutStreamCaching(String uri) throws
Exception {
+ private CamelContext startWithoutStreamCaching(String uri) throws
Exception {
CamelContext cc = context(uri);
cc.setStreamCaching(false);
cc.start();
return cc;
}
- private static void assertStartFails(String uri, String message) throws
Exception {
+ private void assertStartFails(String uri, String message) throws Exception
{
try (CamelContext cc = context(uri)) {
assertThatThrownBy(cc::start)
.rootCause()
@@ -303,6 +325,16 @@ class WasmComponentTest {
}
}
+ /**
+ * The only wasm endpoint of the context.
+ */
+ private static WasmEndpoint wasmEndpoint(CamelContext cc) {
+ List<WasmEndpoint> endpoints = cc.getEndpoints().stream()
+
.filter(WasmEndpoint.class::isInstance).map(WasmEndpoint.class::cast).toList();
+ assertThat(endpoints).hasSize(1);
+ return endpoints.get(0);
+ }
+
private static Exchange send(CamelContext cc, Object body) {
return send(cc.createFluentProducerTemplate().withBody(body));
}
@@ -316,7 +348,8 @@ class WasmComponentTest {
}
/**
- * Whether a thread of a wasm producer (named "... - Wasm" by Camel) is
running guest code.
+ * Whether a thread of a wasm producer (named "... - Wasm" by Camel) is
running guest code. Interpreted or compiled,
+ * guest code is entered through the Endive runtime.
*/
private static boolean moduleRunning() {
return Thread.getAllStackTraces().entrySet().stream()
@@ -493,8 +526,8 @@ class WasmComponentTest {
"base64:AGFzbQEAAAA=, module.wasm",
"gzip:H4sIAAAA/abc, module.wasm" })
void argumentZeroIsTheFileNameOfTheModule(String location, String
expected) {
- assertThat(WasmProducer.programName(location)).isEqualTo(expected);
- assertThat(WasmProducer.arguments(WasmProducer.programName(location),
"-v")).containsExactly(expected, "-v");
+ assertThat(WasmEndpoint.programName(location)).isEqualTo(expected);
+ assertThat(WasmEndpoint.arguments(WasmEndpoint.programName(location),
"-v")).containsExactly(expected, "-v");
}
@Test
@@ -653,7 +686,7 @@ class WasmComponentTest {
@ParameterizedTest
@ValueSource(strings = { "", "?timeout=30000" })
void streamBodyAfterAFailedRunWithStreamCaching(String options) throws
Exception {
- String uri = "wasm:" + module("readexit", READ_EXIT) + options;
+ String uri = endpoint("wasm:" + module("readexit", READ_EXIT) +
options);
AtomicReference<String> seen = new AtomicReference<>();
try (CamelContext cc = new DefaultCamelContext()) {
cc.addRoutes(new RouteBuilder() {
@@ -838,6 +871,105 @@ class WasmComponentTest {
}
}
+ @Test
+ void compileOptionSelectsCompiledCodeOrTheInterpreter() throws Exception {
+ try (CamelContext cc = start("wasm:" + module("trap", TRAP))) {
+ Exchange out = send(cc, "in");
+
+ // the stack of the trap, above the call of _start, shows what ran
the module: the interpreter of Endive, or
+ // the classes its compiler generated for the module
+
assertThat(out.getException()).hasCauseInstanceOf(TrapException.class);
+ List<String> guestFrames =
Arrays.stream(out.getException().getCause().getStackTrace())
+ .map(StackTraceElement::getClassName)
+ .takeWhile(name ->
!name.startsWith("run.endive.runtime.Instance"))
+ .toList();
+ if (compile()) {
+ assertThat(guestFrames).isNotEmpty().allMatch(name ->
name.startsWith(COMPILED_CLASS));
+ } else {
+ assertThat(guestFrames).isNotEmpty()
+ .allMatch(name ->
name.equals("run.endive.runtime.InterpreterMachine"));
+ }
+
assertThat(wasmEndpoint(cc).command().isCompiled()).isEqualTo(compile());
+ }
+ }
+
+ @Test
+ void moduleIsLoadedOncePerEndpoint() throws Exception {
+ String module = module("once", UPPER);
+ try (CamelContext cc = start("wasm:" + module)) {
+ assertThat(text(send(cc, "first"))).isEqualTo("FIRST");
+ WasmEndpoint endpoint = wasmEndpoint(cc);
+ WasiCommand command = endpoint.command();
+ assertThat(command.isCompiled()).isEqualTo(compile());
+
+ // with the file gone, a producer that loaded (and compiled) the
module again would fail to start
+ Files.delete(Path.of(module.substring("file:".length())));
+ WasmProducer second = (WasmProducer) endpoint.createProducer();
+ try {
+ ServiceHelper.startService(second);
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setBody("second");
+ second.process(exchange);
+
+ assertThat(text(exchange)).isEqualTo("SECOND");
+ // and it runs the command of the first producer: the module
was compiled once, not once per producer
+ assertThat(second.command()).isSameAs(command);
+ } finally {
+ ServiceHelper.stopAndShutdownService(second);
+ }
+ }
+ }
+
+ @Test
+ void routeStopAndStartKeepsTheLoadedModule() throws Exception {
+ String module = module("restart", UPPER);
+ try (CamelContext cc = start("wasm:" + module)) {
+ assertThat(text(send(cc, "first"))).isEqualTo("FIRST");
+ WasmEndpoint endpoint = wasmEndpoint(cc);
+ WasiCommand command = endpoint.command();
+
+ cc.getRouteController().stopRoute("wasm");
+ // with the file gone, a route that loaded (and compiled) the
module again would fail to start
+ Files.delete(Path.of(module.substring("file:".length())));
+ cc.getRouteController().startRoute("wasm");
+
+ assertThat(text(send(cc, "second"))).isEqualTo("SECOND");
+ assertThat(wasmEndpoint(cc)).isSameAs(endpoint);
+ assertThat(endpoint.command()).isSameAs(command);
+ }
+ }
+
+ @Test
+ void moduleIsLoadedAgainAfterAFailedLoad() throws Exception {
+ Path file = dir.resolve("later.wasm");
+ try (CamelContext cc = new DefaultCamelContext()) {
+ cc.start();
+ WasmEndpoint endpoint = cc.getEndpoint(endpoint("wasm:file:" +
file.toAbsolutePath()), WasmEndpoint.class);
+
+ // the module is not there yet: the producer cannot be started
+ WasmProducer first = (WasmProducer) endpoint.createProducer();
+ assertThatThrownBy(() -> ServiceHelper.startService(first))
+ .rootCause()
+ .isInstanceOf(FileNotFoundException.class);
+ ServiceHelper.stopAndShutdownService(first);
+
+ // the failure is not kept: once the module is there, the next
producer of the same endpoint loads it
+ Files.write(file, Wat2Wasm.parse(PRELUDE + UPPER + ")"));
+ WasmProducer second = (WasmProducer) endpoint.createProducer();
+ try {
+ ServiceHelper.startService(second);
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setBody("later");
+ second.process(exchange);
+
+ assertThat(text(exchange)).isEqualTo("LATER");
+ assertThat(second.command().isCompiled()).isEqualTo(compile());
+ } finally {
+ ServiceHelper.stopAndShutdownService(second);
+ }
+ }
+ }
+
@Test
void moduleWithoutStartIsRejected() throws Exception {
// the test module of the wasm language exports functions, not a
program
diff --git
a/components/camel-wasm/src/test/java/org/apache/camel/wasm/WasiCommandTest.java
b/components/camel-wasm/src/test/java/org/apache/camel/wasm/WasiCommandTest.java
index 178610edaa39..b6e1f6363896 100644
---
a/components/camel-wasm/src/test/java/org/apache/camel/wasm/WasiCommandTest.java
+++
b/components/camel-wasm/src/test/java/org/apache/camel/wasm/WasiCommandTest.java
@@ -16,27 +16,53 @@
*/
package org.apache.camel.wasm;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
+import org.objectweb.asm.MethodTooLargeException;
import run.endive.runtime.TrapException;
import run.endive.wabt.Wat2Wasm;
import run.endive.wasi.WasiOptions;
import run.endive.wasm.Parser;
+import run.endive.wasm.WasmEngineException;
+import run.endive.wasm.WasmModule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class WasiCommandTest {
+ /** Writes "hi" to standard output. */
+ private static final String HELLO = """
+ (module
+ (import "wasi_snapshot_preview1" "fd_write" (func $fd_write
(param i32 i32 i32 i32) (result i32)))
+ (memory (export "memory") 1)
+ (data (i32.const 16) "hi")
+ (func (export "_start")
+ (i32.store (i32.const 0) (i32.const 16))
+ (i32.store (i32.const 4) (i32.const 2))
+ (drop (call $fd_write (i32.const 1) (i32.const 0) (i32.const
1) (i32.const 8)))))
+ """;
+
+ private static WasmModule module(String wat) {
+ return Parser.parse(Wat2Wasm.parse(wat));
+ }
+
@Test
void randomGetIsServedByASecureRandom() {
- WasiCommand command = new WasiCommand(
- Parser.parse(Wat2Wasm.parse("(module (func (export
\"_start\")))")), List.of("noop.wasm"), 0);
+ WasiCommand command = new WasiCommand(module("(module (func (export
\"_start\")))"), List.of("noop.wasm"), 0);
WasiOptions options = command.options(
InputStream.nullInputStream(),
OutputStream.nullOutputStream(), OutputStream.nullOutputStream(), Map.of());
@@ -48,8 +74,8 @@ class WasiCommandTest {
@Test
void trapOfAModuleWithoutMemory() {
- WasiCommand command = new WasiCommand(
- Parser.parse(Wat2Wasm.parse("(module (func (export \"_start\")
unreachable))")), List.of("trap.wasm"), 0);
+ WasiCommand command
+ = new WasiCommand(module("(module (func (export \"_start\")
unreachable))"), List.of("trap.wasm"), 0);
// the hint about a memory at its maximum does not apply, and looking
for it must not fail
assertThatThrownBy(() -> command.run(
@@ -57,4 +83,224 @@ class WasiCommandTest {
.isInstanceOf(TrapException.class)
.hasMessageNotContaining("maxMemoryPages");
}
+
+ @Test
+ void compiledCodeIsGeneratedOnceAndRunsEveryInstance() {
+ WasiCommand command = new WasiCommand(module(HELLO),
List.of("hello.wasm"), 0, true);
+ assertThat(command.isCompiled()).isTrue();
+
+ GeneratedClasses first = new GeneratedClasses();
+ GeneratedClasses second = new GeneratedClasses();
+ assertThat(command.run(InputStream.nullInputStream(), first,
OutputStream.nullOutputStream(), Map.of())).isZero();
+ assertThat(command.run(InputStream.nullInputStream(), second,
OutputStream.nullOutputStream(), Map.of())).isZero();
+
+ // the two runs are separate instances, executed by the same generated
classes: the module was compiled once
+ assertThat(first.text()).isEqualTo("hi");
+ assertThat(second.text()).isEqualTo("hi");
+ assertThat(first.classes).isNotEmpty();
+ assertThat(second.classes).isEqualTo(first.classes);
+ }
+
+ @Test
+ void interpretedByDefault() {
+ WasiCommand command = new WasiCommand(module(HELLO),
List.of("hello.wasm"), 0);
+ assertThat(command.isCompiled()).isFalse();
+
+ GeneratedClasses out = new GeneratedClasses();
+ assertThat(command.run(InputStream.nullInputStream(), out,
OutputStream.nullOutputStream(), Map.of())).isZero();
+
+ assertThat(out.text()).isEqualTo("hi");
+ assertThat(out.classes).isEmpty();
+ }
+
+ @Test
+ void compileWithoutTheCompilerOnTheClasspathFails() throws Exception {
+ Class<?> isolated = new
WithoutCompiler(WasiCommand.class.getClassLoader()).loadClass(WasiCommand.class.getName());
+ assertThat(isolated).isNotSameAs(WasiCommand.class);
+ var constructor = isolated.getConstructor(WasmModule.class,
List.class, int.class, boolean.class);
+
+ // the interpreter does not need the compiler
+ assertThat(constructor.newInstance(module(HELLO),
List.of("hello.wasm"), 0, false)).isNotNull();
+
+ assertThatThrownBy(() -> constructor.newInstance(module(HELLO),
List.of("hello.wasm"), 0, true))
+ .isInstanceOf(InvocationTargetException.class)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("compile=true needs the Endive runtime
compiler on the classpath (run.endive:compiler")
+
.hasMessageContaining("run/endive/compiler/MachineFactoryCompiler");
+ }
+
+ @Test
+ void functionTooLargeForAJvmMethodIsInterpreted() {
+ // 70000 times "i32.const 1, drop": valid, but more bytecode than a
JVM method can hold (64 KB)
+ byte[] code = new byte[3 * 70_000];
+ for (int i = 0; i < code.length; i += 3) {
+ code[i] = 0x41;
+ code[i + 1] = 1;
+ code[i + 2] = 0x1a;
+ }
+ WasmModule large = Parser.parse(startOnly(code));
+
+ PrintStream systemErr = System.err;
+ ByteArrayOutputStream printed = new ByteArrayOutputStream();
+ WasiCommand command;
+ System.setErr(new PrintStream(printed, true, StandardCharsets.UTF_8));
+ try {
+ command = new WasiCommand(large, List.of("large.wasm"), 0, true);
+ } finally {
+ System.setErr(systemErr);
+ }
+
+ // the module is compiled and works, with the large function
interpreted; Endive did not report that on
+ // System.err, as it does by default: the command logs it instead
+ assertThat(command.isCompiled()).isTrue();
+ assertThat(command.usesInterpreterFallback()).isTrue();
+
assertThat(printed.toString(StandardCharsets.UTF_8)).doesNotContain("interpreted");
+ assertThat(command.run(
+ InputStream.nullInputStream(),
OutputStream.nullOutputStream(), OutputStream.nullOutputStream(), Map.of()))
+ .isZero();
+
+ // a module without such a function is compiled as a whole
+ assertThat(new WasiCommand(module(HELLO), List.of("hello.wasm"), 0,
true).usesInterpreterFallback()).isFalse();
+ }
+
+ @Test
+ void moduleThatCannotBeCompiledFailsWithItsName() {
+ // a vector instruction (v128.const, then drop): valid, but not
supported by the Endive compiler
+ byte[] code = new byte[19];
+ code[0] = (byte) 0xfd;
+ code[1] = 0x0c;
+ code[18] = 0x1a;
+ WasmModule simd = Parser.parse(startOnly(code));
+
+ assertThatThrownBy(() -> new WasiCommand(simd, List.of("simd.wasm"),
0, true))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageStartingWith("The Wasm module simd.wasm cannot be
compiled to JVM bytecode (compile=true): ")
+ .hasCauseInstanceOf(WasmEngineException.class)
+ // the reason is not a function too large for a JVM method, so
the module was not compiled a second
+ // time with the interpreter as fallback
+ .satisfies(e ->
assertThat(WasiCommand.RuntimeCompiler.isFunctionTooLarge(e.getCause())).isFalse());
+ }
+
+ @Test
+ void
onlyAFunctionTooLargeForAJvmMethodIsCompiledAgainWithTheInterpreterAsFallback()
{
+ // how the Endive compiler reports a function too large for a JVM
method
+ MethodTooLargeException tooLarge = new
MethodTooLargeException("Machine", "func_0", "()V", 70_000);
+ WasmEngineException reported = new WasmEngineException("JVM bytecode
too large for WASM method", tooLarge);
+
assertThat(WasiCommand.RuntimeCompiler.isFunctionTooLarge(reported)).isTrue();
+ assertThat(WasiCommand.RuntimeCompiler.isFunctionTooLarge(new
RuntimeException(reported))).isTrue();
+
+ // any other failure is not compiled again
+ assertThat(WasiCommand.RuntimeCompiler.isFunctionTooLarge(new
WasmEngineException("unsupported"))).isFalse();
+ assertThat(WasiCommand.RuntimeCompiler.isFunctionTooLarge(
+ new WasmEngineException("unsupported", new
IllegalArgumentException())))
+ .isFalse();
+ }
+
+ /**
+ * A module in the binary format with one function, exported as _start,
that has no parameters, results or locals
+ * and the given instructions as its body. For bodies that are not
practical to write in WAT.
+ */
+ private static byte[] startOnly(byte[] instructions) {
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+ body.write(0);
+ body.writeBytes(instructions);
+ body.write(0x0b);
+ ByteArrayOutputStream code = new ByteArrayOutputStream();
+ code.write(1);
+ code.writeBytes(unsignedLeb128(body.size()));
+ code.writeBytes(body.toByteArray());
+
+ ByteArrayOutputStream module = new ByteArrayOutputStream();
+ module.writeBytes(new byte[] { 0, 'a', 's', 'm', 1, 0, 0, 0 });
+ // type section: one function type, no parameters and no results
+ section(module, 1, new byte[] { 1, 0x60, 0, 0 });
+ // function section: one function, of type 0
+ section(module, 3, new byte[] { 1, 0 });
+ // export section: function 0 as _start
+ section(module, 7, new byte[] { 1, 6, '_', 's', 't', 'a', 'r', 't', 0,
0 });
+ section(module, 10, code.toByteArray());
+ return module.toByteArray();
+ }
+
+ private static void section(ByteArrayOutputStream module, int id, byte[]
content) {
+ module.write(id);
+ module.writeBytes(unsignedLeb128(content.length));
+ module.writeBytes(content);
+ }
+
+ private static byte[] unsignedLeb128(int value) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ int rest = value;
+ do {
+ int b = rest & 0x7f;
+ rest >>>= 7;
+ out.write(rest != 0 ? b | 0x80 : b);
+ } while (rest != 0);
+ return out.toByteArray();
+ }
+
+ /**
+ * Standard output of a module that records, on every write, the classes
on the call stack that were not loaded by
+ * the class loader of the component: the classes the Endive compiler
generated for the module.
+ */
+ private static final class GeneratedClasses extends OutputStream {
+ private final ByteArrayOutputStream buffer = new
ByteArrayOutputStream();
+ private final Set<Class<?>> classes = new HashSet<>();
+
+ @Override
+ public void write(int b) {
+ write(new byte[] { (byte) b }, 0, 1);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) {
+ ClassLoader own = WasiCommand.class.getClassLoader();
+
classes.addAll(StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .walk(frames ->
frames.map(StackWalker.StackFrame::getDeclaringClass)
+ .filter(c -> c.getClassLoader() != null &&
c.getClassLoader() != own)
+ .takeWhile(c ->
!c.getName().startsWith("org.junit."))
+ .collect(Collectors.toSet())));
+ buffer.write(b, off, len);
+ }
+
+ String text() {
+ return buffer.toString(StandardCharsets.UTF_8);
+ }
+ }
+
+ /**
+ * Loads its own copy of the classes of org.apache.camel.wasm, on a
classpath without the Endive compiler.
+ */
+ private static final class WithoutCompiler extends ClassLoader {
+
+ WithoutCompiler(ClassLoader parent) {
+ super(parent);
+ }
+
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
+ if (name.startsWith("run.endive.compiler.")) {
+ throw new ClassNotFoundException(name);
+ }
+ if (!name.startsWith("org.apache.camel.wasm.")) {
+ return super.loadClass(name, resolve);
+ }
+ synchronized (getClassLoadingLock(name)) {
+ Class<?> loaded = findLoadedClass(name);
+ if (loaded == null) {
+ try (InputStream is =
getParent().getResourceAsStream(name.replace('.', '/') + ".class")) {
+ if (is == null) {
+ throw new ClassNotFoundException(name);
+ }
+ byte[] bytes = is.readAllBytes();
+ loaded = defineClass(name, bytes, 0, bytes.length);
+ } catch (IOException e) {
+ throw new ClassNotFoundException(name, e);
+ }
+ }
+ return loaded;
+ }
+ }
+ }
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index ff5caf83b25d..719db94379d8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -209,6 +209,11 @@ module.
* `WasmSupport.deserialize(byte[], Exchange)`.
* The constructor `WasmProducer(Endpoint, String, String)` is replaced by
`WasmProducer(WasmEndpoint)`.
+`camel-wasm` has a new dependency, `run.endive:compiler`, which brings in ASM
(`org.ow2.asm`). It is used by the
+new `compile` option of the endpoint (default `false`). The module is now
loaded once per endpoint and no longer by
+every producer, so stopping and starting a route does not read the module
again: remove the route and add it
+again, or restart the application, to load a module that has changed.
+
=== camel-vertx-http - a header with several values is sent as several headers
A message header whose value is a `java.util.Collection` or an object array
(as the HTTP consumers map a request
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WasmEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WasmEndpointBuilderFactory.java
index 3ead57c9daca..8e8cc3b8d165 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WasmEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WasmEndpointBuilderFactory.java
@@ -64,6 +64,54 @@ public interface WasmEndpointBuilderFactory {
doSetProperty("args", args);
return this;
}
+ /**
+ * Whether to compile the module to JVM bytecode, with the runtime
+ * compiler of Endive, instead of interpreting it. The module is
+ * compiled once per endpoint, when the first producer of the endpoint
+ * is initialized (with the route, or at the first message with
+ * lazyStartProducer), which takes time for a large module; every
+ * exchange then runs the compiled code, which is much faster than the
+ * interpreter. Stopping and starting the route does not compile the
+ * module again. All other options work the same way. The compiler
+ * generates classes at run time, so this option cannot be used in a
+ * GraalVM native image.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: producer
+ *
+ * @param compile the value to set
+ * @return the dsl builder
+ */
+ default WasmEndpointBuilder compile(boolean compile) {
+ doSetProperty("compile", compile);
+ return this;
+ }
+ /**
+ * Whether to compile the module to JVM bytecode, with the runtime
+ * compiler of Endive, instead of interpreting it. The module is
+ * compiled once per endpoint, when the first producer of the endpoint
+ * is initialized (with the route, or at the first message with
+ * lazyStartProducer), which takes time for a large module; every
+ * exchange then runs the compiled code, which is much faster than the
+ * interpreter. Stopping and starting the route does not compile the
+ * module again. All other options work the same way. The compiler
+ * generates classes at run time, so this option cannot be used in a
+ * GraalVM native image.
+ *
+ * The option will be converted to a <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: producer
+ *
+ * @param compile the value to set
+ * @return the dsl builder
+ */
+ default WasmEndpointBuilder compile(String compile) {
+ doSetProperty("compile", compile);
+ return this;
+ }
/**
* Environment variables for the module, for example
* environment.LOG_LEVEL=debug. The module sees no environment
variables