Table of Contents

    Book an Appointment

    How Did We Discover the Need to Load External WASM in a Kotlin Web App?

    While working on a high-throughput SaaS FinTech platform, our engineering team was tasked with modernizing the architecture using Kotlin Multiplatform (KMP). The goal was to share business logic across a native desktop application and a web-based client portal. During this project, we encountered a situation where a core piece of financial calculation logic—originally written in C++ for maximum performance—was provided as a pre-compiled, third-party WebAssembly (WASM) module for the web tier.

    On the desktop side, integrating the native library was straightforward using KMP’s robust C-interop capabilities. However, when we shifted our focus to the web target, we hit a wall. The official Kotlin Multiplatform documentation extensively covers adding dependencies for Android and iOS, but it is noticeably silent on how to load and execute a raw, pre-compiled WASM module as a dependency within a KMP web application.

    We realized that without a clear path to bind this external WASM module to our Kotlin web target, the web version of the platform would lack critical functionality. This challenge required us to look beyond standard dependency management and engineer a custom interop bridge. This article details how we identified the right approach and successfully integrated the module, providing a blueprint for other teams facing similar documentation gaps.

    Why Was Adding a Pre-Compiled WASM Module Critical for This Architecture?

    The business use case centered around a proprietary risk-assessment algorithm. This algorithm performed thousands of real-time calculations based on user input. For the desktop flavor of our application, the heavy lifting was handled by an installed native binary. For the web application, rewriting the complex, highly optimized C++ algorithm into native Kotlin was neither feasible nor cost-effective due to strict compliance and auditing requirements.

    Instead, the third-party provider supplied a pre-compiled .wasm file. In our architecture, the KMP shared module was responsible for orchestrating data flow, meaning the shared Kotlin code needed a unified interface to call this risk-assessment engine regardless of the platform. On the web target (compiled to JS or Kotlin/Wasm), the application had to fetch, instantiate, and securely call the exported functions of this external WASM module without blocking the main browser thread.

    What Were the Main Bottlenecks in Calling Another WASM Module from KMP?

    When attempting to integrate the external module, several symptoms and architectural oversights immediately surfaced:

    • Missing Build Tooling: Standard Gradle implementation() blocks in KMP do not natively support injecting raw .wasm files into the web dependency tree.
    • Instantiation Timing: WebAssembly modules must be instantiated asynchronously (typically via WebAssembly.instantiateStreaming). Kotlin’s synchronous application initialization crashed because it attempted to call the WASM functions before the browser had fully loaded the binary.
    • Interop Restrictions: Kotlin cannot directly call a separate, standalone WASM memory space. Since the KMP app and the third-party module were compiled separately, they did not share a memory allocator. Passing complex data types (like strings or arrays) between Kotlin and the external WASM module caused memory boundary violations and silent browser console errors.

    How Did We Approach Loading and Calling the External WASM Module?

    To resolve this, we mapped out the execution lifecycle of the web application and evaluated several diagnostic steps. We knew we had to treat the external WASM module not as a traditional Kotlin dependency, but as a dynamic browser asset. We considered these solutions as well:

    Did We Consider Wrapping the WASM Module in an NPM Package?

    Our first thought was to wrap the .wasm file in a custom NPM package with JavaScript bindings, and then import it using KMP’s @JsModule. While this would satisfy the Gradle build process, it introduced unnecessary overhead and tightly coupled our build pipeline to a localized NPM registry, which complicated our CI/CD workflows.

    What About Rewriting the Logic Directly in Kotlin?

    We briefly analyzed the cost of reverse-engineering the risk-assessment algorithm and rewriting it in pure Kotlin. This was quickly discarded. Not only would it violate the intellectual property agreements with the third-party provider, but it would also introduce severe compliance risks and diverge from the validated mathematical models already present in the pre-compiled module.

    Could We Use Standard Kotlin Multiplatform Dependencies for Web?

    We dug deep into the KMP documentation to see if a recent update supported WASM-to-WASM linking. Unfortunately, at the time of the project, dynamic linking between disparate WASM modules in a KMP environment was not natively supported. KMP manages its own WASM memory, and directly linking an external C++ WASM module requires an intermediary bridge.

    We concluded that the most resilient solution was to leverage JavaScript as the universal bridge. By loading the WASM module via standard Web APIs and exposing its exports to the global window object, we could use Kotlin’s external declarations to safely interact with it.

    How Do You Actually Implement and Bind a Third-Party WASM Module in KMP?

    Our final implementation involved a three-step orchestration process: asset hosting, asynchronous JavaScript bootstrapping, and Kotlin external binding. This approach ensured that the WASM module was fully instantiated before the Kotlin application booted.

    Step 1: The JavaScript Bootstrapper

    We placed the calculator.wasm file in the public web assets directory. Before loading the Kotlin compiled script, we added a small JavaScript bootstrapper to the index.html.

    // bootstrapper.js
    window.ExternalEngine = {
        exports: null,
        isLoaded: false,
        load: async function() {
            try {
                const response = await fetch('calculator.wasm');
                const wasmModule = await WebAssembly.instantiateStreaming(response, {});
                this.exports = wasmModule.instance.exports;
                this.isLoaded = true;
                console.log("External WASM module loaded successfully.");
            } catch (error) {
                console.error("Failed to load external WASM module:", error);
            }
        }
    };
    

    Step 2: Halting Kotlin Initialization

    We modified our main HTML file to ensure the Kotlin app only launched after the WASM promise resolved.

    <script src="bootstrapper.js"></script>
    <script>
        window.ExternalEngine.load().then(() => {
            // Load the Kotlin compiled JS/WASM payload dynamically
            const script = document.createElement('script');
            script.src = 'our-kmp-app.js';
            document.body.appendChild(script);
        });
    </script>
    

    Step 3: Kotlin External Declarations

    Within the KMP jsMain (or wasmJsMain) source set, we declared external functions mapped to the JavaScript bridge. Because we couldn’t pass Kotlin objects directly into the C++ WASM memory space, we restricted our interop to primitive types (Doubles and Integers) which safely cross the JS/WASM boundary.

    // Kotlin Multiplatform JS/Wasm Source Set
    external object ExternalEngine {
        val isLoaded: Boolean
        val exports: WasmExports
    }
    external interface WasmExports {
        // Maps directly to the exported C++ function
        fun calculate_risk_score(inputValue: Double): Double
    }
    fun getRiskScore(input: Double): Double {
        if (!ExternalEngine.isLoaded) {
            throw IllegalStateException("WASM Engine not initialized")
        }
        return ExternalEngine.exports.calculate_risk_score(input)
    }
    

    Performance and Security Considerations: By limiting the interop to primitive types, we avoided the high CPU overhead of copying memory buffers between the KMP WASM instance and the external WASM instance. Furthermore, hosting the .wasm file locally alongside our assets prevented CORS issues and ensured strict content security policies (CSP) were maintained.

    What Can Engineering Teams Learn About Integrating External WebAssembly?

    When engineering leaders push to modernize platforms, encountering undocumented edge cases is inevitable. Here are the core insights from this deployment:

    • Use JavaScript as the Interop Bridge: Until WASM-to-WASM dynamic linking matures, JavaScript remains the safest and most reliable bridge between isolated WebAssembly modules.
    • Control the Initialization Lifecycle: Never assume synchronous loading for web assets. Always suspend the launch of your KMP application until all external WebAssembly modules are confirmed as instantiated.
    • Mind the Memory Boundary: Separate WASM modules maintain separate linear memory. Passing complex strings or arrays requires shared memory buffers or serialization (like JSON). Stick to primitives when possible.
    • Strategic Talent Allocation: Building seamless cross-platform bridges requires deep knowledge of both native memory models and web execution contexts. When companies hire software developer teams, they must ensure the engineers understand the underlying compilation targets, not just the Kotlin syntax.
    • Isolate Platform Logic: Use KMP’s expect/actual paradigm to completely hide this Web/JS interop complexity from your shared business logic.
    • Bridge Documentation Gaps with Prototypes: When official docs fall short, isolate the problem. Build a minimal HTML/JS/WASM prototype before integrating it into the complex KMP build system. If you need to hire kotlin developers for multiplatform web initiatives, look for those who can independently prototype solutions outside the framework.

    How Can You Summarize This KMP WASM Integration Challenge?

    Integrating a pre-compiled third-party WASM module into a Kotlin Multiplatform web application initially seemed like a blocking architectural issue due to a lack of explicit documentation. By stepping back and leveraging the browser’s native JavaScript WebAssembly API as an initialization bridge, we successfully bypassed the build tooling limitations. We were able to inject high-performance, compliant native calculations into our web target without compromising the shared KMP architecture. For organizations looking to modernize complex enterprise systems, having engineers who can navigate these low-level interop challenges is critical. If your project requires sophisticated cross-platform architecture, contact us to explore how we can help you hire frontend developers for custom architecture and dedicated remote engineering teams.

    Social Hashtags

    #KotlinMultiplatform #KMP #WebAssembly #WASM #Kotlin #KotlinWasm #WebDevelopment #SoftwareArchitecture #JavaScript #CrossPlatformDevelopment #DeveloperTutorial #Programming

     

    Frequently Asked Questions

    Success Stories That Inspire

    See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.