Quarkus Chicory - An extension to easily integrate Chicory in your Quarkus applications

Overview

Quarkus Chicory integrates the Chicory WebAssembly runtime into Quarkus applications, providing a seamless way to execute WebAssembly modules within your Java applications. The extension abstracts away the complexity of WASM module management, compilation strategies, and execution modes through a simple injectable API.

Core Features

WasmQuarkusContext - The Central API

The WasmQuarkusContext is the primary interface between your application and WebAssembly modules. It provides environment-aware access to WASM modules through CDI injection:

@Inject
@Named("my-module")
WasmQuarkusContext wasmContext;

What it provides:

  • MachineFactory - The execution engine configured for your environment:

    • In dev mode: configured for runtime compilation to enable hot reload

    • In production: optimized for build-time compilation

    • In native image: configured for native execution

  • WasmModule - The parsed WebAssembly module:

    • Loaded from configuration (file path or classpath resource)

    • Cached and reused across requests

    • Automatically reloaded in dev mode when source changes

Environment-aware behavior:

The WasmQuarkusContext automatically adapts based on:

  • Quarkus execution mode (dev, test, production, native)

  • Module configuration (static vs dynamic)

  • Performance settings (interpreter, runtime compiler, build-time compiler)

This means you write your code once, and the extension optimizes execution for each environment:

@PostConstruct
public void init() {
    // Same code works in dev, production, and native image
    // Extension provides the right MachineFactory for each
    Instance instance = Instance.builder(wasmContext.getWasmModule())
        .withMachineFactory(wasmContext.getMachineFactory())
        .build();
}

Build-Time Code Generation

The extension generates Java bytecode from your WebAssembly modules at build time:

  • Replaces Chicory Maven plugin - no separate plugin needed

  • Type-safe access to exported functions through generated classes

  • Optimized performance through build-time compilation

  • Better IDE integration with code completion and type checking

Generated code is automatically available in the build output.

Dependency Management

Automatically handles version alignment between Quarkus and Chicory’s ASM dependencies:

  • No dependency conflicts - transparent version management

  • Just add the extension - no manual dependency configuration

  • Automatic updates when upgrading Quarkus or the extension

Multi WASM Module Support

Configure and manage multiple WebAssembly modules, each with its own WasmQuarkusContext:

quarkus.chicory.modules.module-a.wasm-file=module-a.wasm
quarkus.chicory.modules.module-b.wasm-file=module-b.wasm
@Inject @Named("module-a") WasmQuarkusContext moduleA;
@Inject @Named("module-b") WasmQuarkusContext moduleB;

Each module is independently configured with its own MachineFactory and WasmModule.

Static and Dynamic Module Loading

Static Configuration

Modules configured through application.properties with WasmQuarkusContext provided as injectable beans:

quarkus.chicory.modules.my-module.name=com.example.MyModule
quarkus.chicory.modules.my-module.wasm-file=${project.basedir}/src/main/resources/my-module.wasm

Benefits: - WasmQuarkusContext injected automatically - MachineFactory optimized for build-time compilation - Automatic live reload in dev mode - Native image support

Dynamic Loading

For runtime-loaded modules, you can manually create instances using the appropriate MachineFactory from existing WasmQuarkusContext beans, or configure execution mode through properties.

Intelligent Execution Mode Selection

The extension configures the MachineFactory based on environment:

Development Mode (quarkus:dev)

  • MachineFactory: Runtime compiler for hot reload

  • WasmModule: Reloaded automatically when files change

  • Live reload: Edit WASM → instant reload → test immediately

Production Mode

  • MachineFactory: build-time compiler for optimal performance

  • WasmModule: Compiled at build time, cached

  • Optimized execution: Pre-compiled JVM bytecode

Native Image Mode

  • MachineFactory: Native compilation

  • WasmModule: Embedded in native executable

  • Fast startup: No runtime compilation overhead

Live Reload in Development

Static modules automatically watched and reloaded:

  1. Edit WASM source code

  2. Rebuild to .wasm file

  3. Extension detects change

  4. New WasmModule loaded with runtime compiler MachineFactory

  5. Test immediately - no restart needed

Native Image Compatibility

Full GraalVM native image support with build-time WASM compilation through the extension’s native-aware MachineFactory.

Note on Chicory Annotations

The quarkus-chicory extension does not currently expose Chicory’s annotation processing capabilities (@HostModule/@WasmExport) to users. The extension focuses on:

  • Injectable WasmQuarkusContext beans

  • Automatic code generation from WASM modules

  • Environment-optimized MachineFactory configuration

If you want to use Chicory’s @HostModule/@WasmExport annotations in a Quarkus application, you would need to manually add the Chicory annotation processor dependencies to your project.

Examples

Basic Static Configuration

This example demonstrates how to configure and use a static WebAssembly module loaded at build time.

Configuration

quarkus.chicory.modules.operation-static.name=io.quarkiverse.chicory.it.OperationModule
# The Wasm module payload is configurable either as a file
##quarkus.chicory.modules.operation.wasm-file=src/main/resources/wasm/operation.wasm
# Or as a classpath resource, but file the file based configuration takes precedence
quarkus.chicory.modules.operation-static.wasm-resource=operation.wasm

# Quarkus Chicory will watch all configured Wasm modules, so no need to add the following property:
#quarkus.live-reload.watched-resources=wasm/operation.wasm

The operation-static identifier is the module name used for injection. The module is loaded as a classpath resource via wasm-resource, and the name property configures code generation.

Java Implementation

The above configuration will let your application inject a WasmQuarkusContext bean, like this:

@Path("/chicory")
@ApplicationScoped
public class ChicoryResource {

    @Inject
    @Named("operation-static")
    WasmQuarkusContext wasmQuarkusContext;

    Instance instance;

    @PostConstruct
    public void init() {
        instance = Instance.builder(wasmQuarkusContext.getWasmModule())
            .withMachineFactory(wasmQuarkusContext.getMachineFactory())
            .build();
    }

    @GET
    public Response hello() {
        var result = instance.export("operation").apply(41, 1);
        return Response.ok("Hello chicory: " + result[0]).build();
    }
}

Exported Functions - Calling WASM from Java

This example demonstrates how to call functions exported by your WebAssembly modules.

Configuration

Configure your WASM module in application.properties:

quarkus.chicory.modules.calculator.name=io.quarkiverse.chicory.it.CalculatorModule
quarkus.chicory.modules.calculator.wasm-file=${project.basedir}/src/main/resources/calculator.wasm

WASM Module Exports

Your WebAssembly module exports functions that can be called from Java:

;; WebAssembly Text Format (WAT)
(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)

  (func (export "multiply") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.mul)

  (func (export "sqrt") (param f64) (result f64)
    local.get 0
    f64.sqrt)
)

Java Implementation

@Path("/calculator")
@ApplicationScoped
public class CalculatorResource {

    @Inject
    @Named("calculator")
    WasmQuarkusContext wasmQuarkusContext;

    Instance instance;

    // Cache exported functions for better performance
    ExportFunction addFunction;
    ExportFunction multiplyFunction;
    ExportFunction sqrtFunction;

    @PostConstruct
    public void init() {
        instance = Instance.builder(wasmQuarkusContext.getWasmModule())
            .withMachineFactory(wasmQuarkusContext.getMachineFactory())
            .build();

        // Export functions by name
        addFunction = instance.export("add");
        multiplyFunction = instance.export("multiply");
        sqrtFunction = instance.export("sqrt");
    }

    @GET
    @Path("/add/{a}/{b}")
    public Response add(@PathParam("a") int a, @PathParam("b") int b) {
        // Call exported function with parameters
        long[] result = addFunction.apply(a, b);
        return Response.ok(result[0]).build();
    }

    @GET
    @Path("/multiply/{a}/{b}")
    public Response multiply(@PathParam("a") int a, @PathParam("b") int b) {
        long[] result = multiplyFunction.apply(a, b);
        return Response.ok(result[0]).build();
    }

    @GET
    @Path("/sqrt/{n}")
    public Response sqrt(@PathParam("n") double n) {
        // Use the alternative exports() API
        var result = instance.exports()
            .function("sqrt")
            .apply(Value.fromDouble(n));

        return Response.ok(result[0].asDouble()).build();
    }
}

Dynamic Module Loading

This example demonstrates how to load WebAssembly modules dynamically at runtime, useful for plugin systems, multi-tenant applications, or user-uploaded WASM modules.

Configuration

Configure a dynamic module without specifying the WASM payload in application.properties:

# Define the module but don't specify wasm-file or wasm-resource
quarkus.chicory.modules.operation-dynamic.name=io.quarkiverse.chicory.it.DynamicOperationModule

# The Wasm module payload will be provided at runtime

The key difference from static modules is the absence of wasm-file or wasm-resource properties. The extension creates the WasmQuarkusContext bean, but the actual WASM module is loaded later.

Java Implementation

@Path("/chicory/dynamic")
@ApplicationScoped
public class ChicoryDynamicResource {

    @Inject
    @Named("operation-dynamic")
    WasmQuarkusContext wasmQuarkusContext;  // Injected but no WASM loaded yet

    Instance instance;

    @POST
    @Path("/upload")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response upload(@RestForm("module") FileUpload wasmModule) throws IOException {
        try (InputStream is = Files.newInputStream(wasmModule.uploadedFile())) {
            // Parse the uploaded WASM file
            WasmModule module = Parser.parse(is.readAllBytes());

            // Create instance using the MachineFactory from WasmQuarkusContext
            instance = Instance.builder(module)
                .withMachineFactory(wasmQuarkusContext.getMachineFactory())  // Environment-optimized
                .build();

            return Response.ok("Module loaded successfully").build();
        }
    }

    @GET
    public Response execute() {
        if (instance == null) {
            return Response.status(405)
                .entity("No module loaded. Upload one first via POST /upload")
                .build();
        }

        // Call the exported function
        var result = instance.export("operation").apply(41, 1);
        return Response.ok("Result: " + result[0]).build();
    }
}

Host Imports - Calling Java from WebAssembly

This example demonstrates how to provide Java functions that WebAssembly modules can call (host imports).

Configuration

Configure your WASM module in application.properties:

quarkus.chicory.modules.operation.name=io.quarkiverse.chicory.it.OperationModule
quarkus.chicory.modules.operation.wasm-resource=operation.wasm

Java Implementation

Inject the module and provide host functions that the WASM module can import:

@Path("/chicory")
@ApplicationScoped
public class ChicoryResourceWithImports {

    @Inject
    @Named("operation")
    WasmQuarkusContext wasmQuarkusContext;

    Instance instance;

    @PostConstruct
    public void init() {
        WasmModule wasmModule = wasmQuarkusContext.getWasmModule();

        // Define a host function that WASM can call
        HostFunction hostLog = new HostFunction(
            "env",                                    // Module name
            "host_log",                               // Function name
            FunctionType.of(List.of(ValType.I32), List.of()),  // Signature: (i32) -> ()
            (inst, args) -> {
                int num = (int) args[0];
                System.out.println("Called from WASM: " + num);
                return null;
            }
        );

        // Build instance with host imports
        instance = Instance.builder(wasmModule)
            .withImportValues(ImportValues.builder()
                .addFunction(hostLog)
                .build())
            .withMachineFactory(wasmQuarkusContext.getMachineFactory())
            .build();
    }

    @GET
    public Response hello() {
        // Call WASM function, which will call back to our host_log function
        var result = instance.exports().function("operation").apply(41, 1);
        return Response.ok("Result: " + result[0]).build();
    }
}

WASI Integration with Exported Functions

This example shows how to integrate WASI (WebAssembly System Interface) support for modules that need system access.

Configuration

Configure the module in application.properties:

quarkus.chicory.modules.qrcode.name=io.quarkiverse.chicory.it.QRCodeModule
quarkus.chicory.modules.qrcode.wasm-file=${project.basedir}/src/main/resources/qr-generator.wasm

Java Implementation

Integrate WASI with the Quarkus-provided MachineFactory:

@Path("/qrcode")
@ApplicationScoped
public class QRCodeResource {

    @Inject
    @Named("qrcode")
    WasmQuarkusContext wasmQuarkusContext;

    Instance instance;
    ExportFunction malloc;
    ExportFunction free;
    ExportFunction generateQR;
    Memory memory;

    @PostConstruct
    public void init() throws IOException {
        WasmModule wasmModule = wasmQuarkusContext.getWasmModule();

        // Create WASI support
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        ByteArrayOutputStream stderr = new ByteArrayOutputStream();

        WasiOptions options = WasiOptions.builder()
            .withStdout(stdout)
            .withStderr(stderr)
            .build();

        WasiPreview1 wasi = WasiPreview1.builder()
            .withOptions(options)
            .build();

        Store store = new Store().addFunction(wasi.toHostFunctions());

        // Combine WASI with Quarkus MachineFactory
        instance = Instance.builder(wasmModule)
            .withMachineFactory(wasmQuarkusContext.getMachineFactory())  // Environment-optimized
            .withImportValues(store.toImportValues())                     // WASI functions
            .withStart(false)
            .build();

        // Get exported functions
        malloc = instance.export("malloc");
        free = instance.export("free");
        generateQR = instance.export("generateQR");
        memory = instance.memory();

        // Initialize WASM module (if needed)
        try {
            instance.export("_start").apply();
        } catch (WasiExitException e) {
            if (e.exitCode() != 0) {
                throw new RuntimeException("WASM initialization failed");
            }
        }
    }

    @GET
    public Response generate(@QueryParam("text") String text) {
        byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);

        // Allocate memory in WASM
        int textPtr = (int) malloc.apply(textBytes.length)[0];

        try {
            // Write data to WASM memory
            memory.write(textPtr, textBytes);

            // Call exported function
            long[] result = generateQR.apply(textPtr, textBytes.length);
            int qrPtr = (int) result[0];

            // Read result from WASM memory
            byte[] qrCode = memory.readBytes(qrPtr, result[1]);

            return Response.ok(qrCode).build();
        } finally {
            free.apply(textPtr);
        }
    }
}

Memory Management - Passing Complex Data

This example demonstrates memory management patterns for passing complex data (strings, byte arrays) between Java and WebAssembly.

Configuration

quarkus.chicory.modules.go-cel.name=io.quarkiverse.chicory.it.GoCelModule
quarkus.chicory.modules.go-cel.wasm-file=${project.basedir}/src/main/resources/go-cel.wasm

WASM Module Requirements

The WASM module must export memory management functions:

  • malloc(size: i32) → i32 - Allocate memory, returns pointer

  • free(ptr: i32) - Free allocated memory

  • evalPolicy(policyPtr: i32, policyLen: i32, inputPtr: i32, inputLen: i32) → i32 - Process data using pointer/length pairs

Java Implementation

Use the exported malloc/free functions and memory access:

@Path("/policy")
@ApplicationScoped
public class PolicyResource {

    @Inject
    @Named("go-cel")
    WasmQuarkusContext wasmQuarkusContext;

    Instance instance;
    ExportFunction malloc;
    ExportFunction free;
    ExportFunction evalPolicy;
    Memory memory;

    @PostConstruct
    public void init() throws IOException {
        WasmModule wasmModule = wasmQuarkusContext.getWasmModule();

        // Configure WASI if needed
        WasiPreview1 wasi = WasiPreview1.builder()
            .withOptions(WasiOptions.builder().build())
            .build();

        Store store = new Store().addFunction(wasi.toHostFunctions());

        instance = Instance.builder(wasmModule)
            .withMachineFactory(wasmQuarkusContext.getMachineFactory())
            .withImportValues(store.toImportValues())
            .withStart(false)
            .build();

        // Export memory management functions
        malloc = instance.export("malloc");
        free = instance.export("free");
        evalPolicy = instance.export("evalPolicy");
        memory = instance.memory();

        // Initialize Go runtime
        try {
            instance.export("_start").apply();
        } catch (WasiExitException e) {
            if (e.exitCode() != 0) throw new RuntimeException("Init failed");
        }
    }

    @POST
    public Response validate(String policy, String input) {
        byte[] policyBytes = policy.getBytes(StandardCharsets.UTF_8);
        byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);

        // Step 1: Allocate memory in WASM
        int policyPtr = (int) malloc.apply(policyBytes.length)[0];
        int inputPtr = (int) malloc.apply(inputBytes.length)[0];

        try {
            // Step 2: Write data to WASM memory
            memory.write(policyPtr, policyBytes);
            memory.write(inputPtr, inputBytes);

            // Step 3: Call WASM function with pointer/length pairs
            long[] result = evalPolicy.apply(
                policyPtr, policyBytes.length,
                inputPtr, inputBytes.length
            );

            int returnCode = (int) result[0];

            // Step 4: Interpret results
            if (returnCode == 1) {
                return Response.ok("Policy allows").build();
            } else if (returnCode == 0) {
                return Response.ok("Policy denies").build();
            } else {
                return Response.status(400).entity("Error: " + returnCode).build();
            }
        } finally {
            // Step 5: Free allocated memory
            free.apply(policyPtr);
            free.apply(inputPtr);
        }
    }
}

Installation

If you want to use this extension, you need to add the io.quarkiverse:quarkus-chicory extension first to your build file.

For instance, with Maven, add the following dependency to your POM file:

<dependency>
    <groupId>io.quarkiverse.chicory</groupId>
    <artifactId>quarkus-chicory</artifactId>
    <version>0.0.1</version>
</dependency>

Extension Configuration Reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

The Wasm module file to be used. If wasm-resource() is defined too, this has precedence over it.

Environment variable: QUARKUS_CHICORY_MODULES__MODULES__WASM_FILE

string

The Wasm module to be used. If wasm-file() is defined too, it has precedence over this.

Environment variable: QUARKUS_CHICORY_MODULES__MODULES__WASM_RESOURCE

string

The base name to be used for the generated API class.

Environment variable: QUARKUS_CHICORY_MODULES__MODULES__NAME

string

required

The execution mode for a configured Wasm module

Environment variable: QUARKUS_CHICORY_MODULES__MODULES__COMPILER_EXECUTION_MODE

runtime-compiler, interpreter

interpreter

The action to take if the compiler needs to use the interpreter because a function is too big

Environment variable: QUARKUS_CHICORY_MODULES__MODULES__COMPILER_INTERPRETER_FALLBACK

silent, warn, fail

warn

The indexes of functions that should be interpreted, separated by commas

Environment variable: QUARKUS_CHICORY_MODULES__MODULES__COMPILER_INTERPRETED_FUNCTIONS

list of int