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

Reply via email to