从C++到wasm,并在JavaScript中调用

我将为你提供一个完整的从 C++ 编译到 WebAssembly 并在 JavaScript 中调用的指南。

环境准备

1. 安装 Emscripten

# 克隆 emsdk 仓库
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

# 安装最新版本
./emsdk install latest
./emsdk activate latest

# 设置环境变量
source ./emsdk_env.sh

# 验证安装
emcc --version

基础示例

步骤 1:编写 C++ 代码

math_operations.cpp

#include <emscripten.h>
#include <cmath>

// 使用 extern "C" 防止 C++ 名称修饰
extern "C" {

    EMSCRIPTEN_KEEPALIVE
    int add(int a, int b) {
        return a + b;
    }

    EMSCRIPTEN_KEEPALIVE
    double multiply(double a, double b) {
        return a * b;
    }

    EMSCRIPTEN_KEEPALIVE
    double squareRoot(double x) {
        return sqrt(x);
    }

    EMSCRIPTEN_KEEPALIVE
    int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    }
}

步骤 2:编译为 WebAssembly

# 基础编译命令
emcc math_operations.cpp \
  -o math_operations.js \
  -s WASM=1 \
  -s EXPORTED_FUNCTIONS='["_add", "_multiply", "_squareRoot", "_factorial"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'

# 或者使用更现代的 ES6 模块方式
emcc math_operations.cpp \
  -o math_operations.mjs \
  -s WASM=1 \
  -s MODULARIZE=1 \
  -s EXPORT_ES6=1 \
  -s EXPORTED_FUNCTIONS='["_add", "_multiply", "_squareRoot", "_factorial"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'

在 JavaScript 中调用

方式 1:使用传统胶水代码

index.html

<!DOCTYPE html>
<html>
<head>
    <title>WASM C++ 示例</title>
</head>
<body>
    <h1>WebAssembly C++ 函数调用示例</h1>
    
    <div>
        <button onclick="testAdd()">测试加法</button>
        <button onclick="testMultiply()">测试乘法</button>
        <button onclick="testSquareRoot()">测试平方根</button>
        <button onclick="testFactorial()">测试阶乘</button>
    </div>

    <div id="output" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc;"></div>

    <!-- 加载 Emscripten 生成的 JS 胶水代码 -->
    <script src="math_operations.js"></script>
    <script>
        let add, multiply, squareRoot, factorial;

        // 等待 WASM 模块初始化完成
        Module.onRuntimeInitialized = function() {
            // 使用 cwrap 包装 C++ 函数
            add = Module.cwrap('add', 'number', ['number', 'number']);
            multiply = Module.cwrap('multiply', 'number', ['number', 'number']);
            squareRoot = Module.cwrap('squareRoot', 'number', ['number']);
            factorial = Module.cwrap('factorial', 'number', ['number']);

            logOutput("WASM 模块加载完成!");
        };

        function testAdd() {
            if (!add) return;
            const result = add(15, 27);
            logOutput(`add(15, 27) = ${result}`);
        }

        function testMultiply() {
            if (!multiply) return;
            const result = multiply(3.14, 2.5);
            logOutput(`multiply(3.14, 2.5) = ${result}`);
        }

        function testSquareRoot() {
            if (!squareRoot) return;
            const result = squareRoot(16);
            logOutput(`squareRoot(16) = ${result}`);
        }

        function testFactorial() {
            if (!factorial) return;
            const result = factorial(5);
            logOutput(`factorial(5) = ${result}`);
        }

        function logOutput(message) {
            const output = document.getElementById('output');
            output.innerHTML += message + '<br>';
        }
    </script>
</body>
</html>

方式 2:使用 ES6 模块(推荐)

modern-example.html

<!DOCTYPE html>
<html>
<head>
    <title>WASM ES6 模块示例</title>
</head>
<body>
    <h1>现代 ES6 模块方式</h1>
    <div id="output"></div>

    <script type="module">
        import createModule from './math_operations.mjs';

        class MathWASM {
            constructor() {
                this.module = null;
                this.initialized = false;
            }

            async init() {
                try {
                    this.module = await createModule();
                    this.initialized = true;
                    this.log("WASM 模块初始化成功!");
                    
                    // 包装所有函数
                    this.add = this.module.cwrap('add', 'number', ['number', 'number']);
                    this.multiply = this.module.cwrap('multiply', 'number', ['number', 'number']);
                    this.squareRoot = this.module.cwrap('squareRoot', 'number', ['number']);
                    this.factorial = this.module.cwrap('factorial', 'number', ['number']);
                    
                    this.runTests();
                } catch (error) {
                    this.log(`初始化失败: ${error}`);
                }
            }

            runTests() {
                if (!this.initialized) return;

                this.log(`加法测试: 8 + 12 = ${this.add(8, 12)}`);
                this.log(`乘法测试: 4.5 × 3.2 = ${this.multiply(4.5, 3.2)}`);
                this.log(`平方根测试: √25 = ${this.squareRoot(25)}`);
                this.log(`阶乘测试: 6! = ${this.factorial(6)}`);
            }

            log(message) {
                const output = document.getElementById('output');
                output.innerHTML += message + '<br>';
            }
        }

        // 初始化 WASM 模块
        const mathWASM = new MathWASM();
        mathWASM.init();
    </script>
</body>
</html>

处理字符串和内存操作

C++ 代码(字符串处理)

string_operations.cpp

#include <emscripten.h>
#include <cstring>
#include <string>

extern "C" {

    EMSCRIPTEN_KEEPALIVE
    int stringLength(const char* str) {
        return strlen(str);
    }

    EMSCRIPTEN_KEEPALIVE
    void reverseString(char* str, int length) {
        int start = 0;
        int end = length - 1;
        while (start < end) {
            char temp = str[start];
            str[start] = str[end];
            str[end] = temp;
            start++;
            end--;
        }
    }

    EMSCRIPTEN_KEEPALIVE
    char* concatenateStrings(const char* str1, const char* str2) {
        std::string result = std::string(str1) + std::string(str2);
        char* output = (char*)malloc(result.length() + 1);
        strcpy(output, result.c_str());
        return output;
    }
}

JavaScript 调用字符串函数

string-example.html

<!DOCTYPE html>
<html>
<head>
    <title>WASM 字符串操作</title>
</head>
<body>
    <script src="string_operations.js"></script>
    <script>
        Module.onRuntimeInitialized = function() {
            // 包装函数
            const stringLength = Module.cwrap('stringLength', 'number', ['string']);
            const reverseString = Module.cwrap('reverseString', null, ['number', 'number']);
            const concatenateStrings = Module.cwrap('concatenateStrings', 'number', ['string', 'string']);

            // 测试字符串长度
            const testStr = "Hello WebAssembly";
            console.log(`"${testStr}" 的长度: ${stringLength(testStr)}`);

            // 测试字符串反转
            const strToReverse = "Hello World";
            const lengthBytes = Module.lengthBytesUTF8(strToReverse) + 1;
            const buffer = Module._malloc(lengthBytes);
            Module.stringToUTF8(strToReverse, buffer, lengthBytes);
            
            reverseString(buffer, strToReverse.length);
            const reversed = Module.UTF8ToString(buffer);
            console.log(`反转 "${strToReverse}": ${reversed}`);
            
            Module._free(buffer);

            // 测试字符串连接
            const ptr = concatenateStrings("Hello ", "WASM");
            const concatenated = Module.UTF8ToString(ptr);
            console.log(`连接结果: ${concatenated}`);
            Module._free(ptr); // 记得释放内存
        };
    </script>
</body>
</html>

编译优化选项

生产环境编译

# 优化编译(推荐用于生产环境)
emcc math_operations.cpp \
  -o math_operations.js \
  -O3 \
  -s WASM=1 \
  -s MODULARIZE=1 \
  -s EXPORT_ES6=1 \
  -s EXPORTED_FUNCTIONS='["_add", "_multiply", "_squareRoot", "_factorial"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
  -s ALLOW_MEMORY_GROWTH=1

最小化编译

# 最小化文件大小
emcc math_operations.cpp \
  -o math_operations.js \
  -Oz \
  -s WASM=1 \
  -s MODULARIZE=1 \
  -s SINGLE_FILE=1 \
  -s EXPORTED_FUNCTIONS='["_add", "_multiply", "_squareRoot", "_factorial"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'

运行步骤

  1. 编译 C++ 代码

    emcc math_operations.cpp -o math_operations.js -s WASM=1 -s EXPORTED_FUNCTIONS='["_add", "_multiply", "_squareRoot", "_factorial"]' -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'
    
  2. 启动本地服务器(由于浏览器安全限制,不能直接打开 HTML 文件):

    # 使用 Python
    python -m http.server 8000
    
    # 或使用 Node.js
    npx http-server
    
  3. 在浏览器中访问

    http://localhost:8000
    

关键要点

  1. 使用 extern "C":防止 C++ 名称修饰
  2. 使用 EMSCRIPTEN_KEEPALIVE:确保函数被导出且不被优化掉
  3. 正确编译参数:确保导出需要的函数和运行时方法
  4. 异步加载:WASM 模块需要时间初始化
  5. 内存管理:对于返回指针的函数,记得在 JavaScript 中释放内存

这个完整的流程应该能帮助你成功地将 C++ 代码编译为 WebAssembly 并在 JavaScript 中调用。

posted @ 2025-11-09 14:01  阿木隆1237  阅读(9)  评论(0)    收藏  举报