微信小游戏Wasm

1,616 阅读4分钟

最近折腾了一下wasm,实现了把C++编译成wasm,并在小游戏中加载和使用。因为时间的关系还有一些问题未解决,编译之后有一个警告尚未处理,还未编译生成asm。后面有时间再慢慢完善。

粗略看了一眼文档发现其实还有很多坑需要踩,比如需要注意C++部分的代码优化,C++以及和JS的调用方式,以获得高性能。

测试环境

  • 系统: 开发环境Win10
  • 游戏引擎:Cocos Creator3.8.5
  • 执行环境:微信开发者工具/IOS微信

环境准备

emsdk有比较详细的文档,不详细记录了

参照ninja的官方文档即可,不详细记录了

  • 准备C++文件
// rvo.h
namespace rvo
{
    class Math {
        public:
        Math();
        ~Math();
        int add(int a, int b);
        void toString();
    };
}
// rvo.cpp
#include "rvo.h"
#include <iostream>

namespace rvo
{
    Math::Math() {

    }

    Math::~Math() {

    }

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

    void Math::toString() {
        std::cout << "Math!" << std::endl;
    }
}
// bind.cpp
// 绑定文件,可以手写,也可以让AI生成
#include <emscripten/bind.h>
#include "rvo.h"

using namespace emscripten;

EMSCRIPTEN_BINDINGS(rvo)
{
    class_<rvo::Math>("Math")
        .constructor()
        .function("add", &rvo::Math::add)
        .function("toString", &rvo::Math::toString);
}

emcc命令直接编译,准备构建命令

emcc bind.cpp rvo.cpp -I. -o rvo.js -Oz -s VERBOSE=0 -s WASM=1 -s INITIAL_MEMORY=33554432 -s ALLOW_MEMORY_GROWTH=1 -s DYNAMIC_EXECUTION=0 -s ERROR_ON_UNDEFINED_SYMBOLS=0 -flto --no-entry --bind -s MODULARIZE=1 -s EXPORT_NAME='rvoWasm' -s ENVIRONMENT=web -s FILESYSTEM=0 -s NO_EXIT_RUNTIME=1 -s LLD_REPORT_UNDEFINED -s MIN_SAFARI_VERSION=110000 --closure=1

emcc cmake + ninja 编译

参考自cocos spine wasm CMakeLists.txt

// CMakeLists.txt
cmake_minimum_required(VERSION 3.8)

set(APP_NAME "rvo")

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

project(${APP_NAME}_wasm)

set(BUILD_WASM 1)
set(ENABLE_JSON_PARSER 1)
set(ENABLE_BINARY_PARSER 1)
# set(ENABLE_PROFILING "--profiling")
set(VERBOSE_LOG 0)

set(CMAKE_BUILD_TYPE "MinSizeRel")
# set(CMAKE_BUILD_TYPE "RelWithDebInfo")
# set(CMAKE_BUILD_TYPE "Debug")

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(ENABLE_CLOSURE_COMPILER 0)
    set(RVO_EXTRA_FLAGS "")
else()
    set(ENABLE_CLOSURE_COMPILER 1)
    set(RVO_EXTRA_FLAGS "-Oz")
endif()

if(BUILD_WASM EQUAL 1 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(EVAL_CTOR_FLAG "-s EVAL_CTORS=1")
else()
    set(EVAL_CTOR_FLAG "") # asmjs doesn't support EVAL_CTORS
endif()

# set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${RVO_EXTRA_FLAGS} -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 -DENABLE_JSON_PARSER=${ENABLE_JSON_PARSER} -DENABLE_BINARY_PARSER=${ENABLE_BINARY_PARSER}")
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${RVO_EXTRA_FLAGS} -fno-exceptions -fno-rtti -Wno-inconsistent-missing-override ${ENABLE_PROFILING} \
#    -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 -DENABLE_JSON_PARSER=${ENABLE_JSON_PARSER} -DENABLE_BINARY_PARSER=${ENABLE_BINARY_PARSER}")

message(">>> --------------------------------------------------------------")
message(">>> Current directory: ${CMAKE_CURRENT_LIST_DIR}")
message(">>> CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")
message(">>> ENABLE_CLOSURE_COMPILER: ${ENABLE_CLOSURE_COMPILER}")
message(">>> ENABLE_JSON_PARSER: ${ENABLE_JSON_PARSER}")
message(">>> ENABLE_BINARY_PARSER: ${ENABLE_BINARY_PARSER}")
message(">>> ENABLE_PROFILING: ${ENABLE_PROFILING}")
message(">>> RVO_EXTRA_FLAGS: ${RVO_EXTRA_FLAGS}")
message(">>> EVAL_CTOR_FLAG: ${EVAL_CTOR_FLAG}")
message(">>> --------------------------------------------------------------")
message(">>> CMAKE_C_COMPILER_VERSION is ${CMAKE_C_COMPILER_VERSION}")
message(">>> CMAKE_CXX_COMPILER_VERSION is ${CMAKE_CXX_COMPILER_VERSION}")
message(">>> CMAKE_C_COMPILER_TARGET is ${CMAKE_C_COMPILER_TARGET}")
message(">>> CMAKE_CXX_COMPILER_TARGET is ${CMAKE_CXX_COMPILER_TARGET}")
message(">>> CMAKE_C_PLATFORM_ID is ${CMAKE_C_PLATFORM_ID}")
message(">>> CMAKE_CXX_PLATFORM_ID is ${CMAKE_CXX_PLATFORM_ID}")
message(">>> CMAKE_C_COMPILE_FEATURES is ${CMAKE_C_COMPILE_FEATURES}")
message(">>> CMAKE_CXX_COMPILE_FEATURES is ${CMAKE_CXX_COMPILE_FEATURES}")
message(">>> --------------------------------------------------------------")

include_directories(${CMAKE_CURRENT_LIST_DIR})
file(GLOB RVO_ADAPTER_SRC "${CMAKE_CURRENT_LIST_DIR}/*.cpp")

add_executable(${APP_NAME} ${RVO_ADAPTER_SRC})

# 参数说明
# -Oz 会进一步减小代码大小,可能需要更长的时间才能运行
# --closure 运行 closure 编译器,优化js文件大小
# --no-entry 不导出main函数
# -flto [compile+link] Enables link-time optimizations (LTO).[启用链接时间优化]
# --bind 使用 bindings Api
# -s 选项可以指定为单个参数,在 -s 和选项名称之间带或不带空格 [官方建议不带空格]

# -s WASM=0,-s WASM=1 是否生成wasm文件,否则生成asm.js文件
# -s EVAL_CTORS 将尝试在编译时评估代码[https://emscripten.org/docs/tools_reference/settings_reference.html]
# -s VERBOSE 它会打印出它运行的命令
# -s INITIAL_MEMORY 要使用的初始内存量[https://emscripten.org/docs/tools_reference/settings_reference.html#initial-memory]
# -s ALLOW_MEMORY_GROWTH true时将在运行时无缝且动态地增加内存数组; false时尝试分配的内存超过我们所能分配的内存,则会中止并显示错误
# -s DYNAMIC_EXECUTION=0 禁用动态执行,即禁用eval()和new Function()等动态执行代码的方法,默认值为1
# -s ERROR_ON_UNDEFINED_SYMBOLS 允许未定义的符号
# -s USE_ES6_IMPORT_META 使用 ES6 模块相对导入功能‘import.meta.url’ 自动检测 WASM 模块路径[https://emscripten.org/docs/tools_reference/settings_reference.html#USE_ES6_IMPORT_META]
# -s EXPORT_ES6 使用 ES6 模块导出而不是 UMD 导出[https://emscripten.org/docs/tools_reference/settings_reference.html]
# -s MODULARIZE 是否生成一个模块化的输出,而不是一个全局的 UMD 输出[https://emscripten.org/docs/tools_reference/settings_reference.html#MODULARIZE]
# -s EXPORT_NAME 指定导出的模块名称[https://emscripten.org/docs/tools_reference/settings_reference.html#EXPORT_NAME]
# -s ENVIRONMENT 指定运行时环境[https://emscripten.org/docs/tools_reference/settings_reference.html#ENVIRONMENT]
# -s FILESYSTEM 是否启用文件系统[https://emscripten.org/docs/tools_reference/settings_reference.html#FILESYSTEM]
# -s NO_EXIT_RUNTIME 禁用运行时退出[https://emscripten.org/docs/tools_reference/settings_reference.html#NO_EXIT_RUNTIME]
# -s LLD_REPORT_UNDEFINED 使用LLD报告未定义的符号[https://emscripten.org/docs/tools_reference/settings_reference.html#LLD_REPORT_UNDEFINED]
# -s MIN_SAFARI_VERSION 指定Safari浏览器最低版本[https://emscripten.org/docs/tools_reference/settings_reference.html#min-safari-version]

# -s EMBIND_AOT=0 -s MALLOC=emmalloc
set(EMS_LINK_FLAGS "${RVO_EXTRA_FLAGS} ${EVAL_CTOR_FLAG} -s VERBOSE=${VERBOSE_LOG} -s WASM=${BUILD_WASM} -s INITIAL_MEMORY=33554432 -s ALLOW_MEMORY_GROWTH=1 -s DYNAMIC_EXECUTION=0 -s ERROR_ON_UNDEFINED_SYMBOLS=0 \
        -flto --no-entry --bind -s MODULARIZE=1 -s EXPORT_NAME='rvoWasm' \
        -s ENVIRONMENT=web -s FILESYSTEM=0 -s NO_EXIT_RUNTIME=1 -s LLD_REPORT_UNDEFINED \
        -s MIN_SAFARI_VERSION=110000 \
        --closure=${ENABLE_CLOSURE_COMPILER}")
        
set_target_properties(${APP_NAME} PROPERTIES CXX_STANDARD 11 LINK_FLAGS ${EMS_LINK_FLAGS})

cmake生成ninja工程

emcmake cmake

ninja 编译/链接生成*.js,*.wasm, *.asm

emmake ninja
[3/3] Linking CXX executable rvo.js
building:INFO: ctor_evaller: trying to eval global ctors (--ctors=__wasm_call_ctors)
building:INFO:

trying to eval __wasm_call_ctors
  ...partial evalling successful, but stopping since could not eval: call import: env._embind_register_class
  ...stopping

构建产物

rvo.wasm;rvo.js 两个文件

客户端加载和调用

新建一个小游戏分包: 3rd

|--3rd

|-- rvo.wasm

rvo.js emsdk编译生成的文件(放在首包

rvo.d.ts 手写声明文件(与rvo.js在同一层目录

// instantiated.ts
import { assetManager } from 'cc';
import { error } from 'cc';
import { AssetManager } from 'cc';
import { default as rvoWasm } from './rvo.js'
import { sys } from 'cc';

// NOTE: The global variable `CCWebAssembly` is assigned in platforms/(bytedance|wechat)/wrapper/builtin/index.js
declare namespace CCWebAssembly {
    // The first argument of `instantiate` function in mini-game platforms is always a wasm url.
    function instantiate(url: string, importObject?: WebAssembly.Imports): Promise<WebAssembly.WebAssemblyInstantiatedSource>
}

let promiseToLoadWasmModule: Promise<AssetManager.Bundle> | undefined;
function loadSubpackage (name: string): Promise<AssetManager.Bundle> {
    if (promiseToLoadWasmModule) {
        return promiseToLoadWasmModule
    }
    promiseToLoadWasmModule = new Promise<any>((resolve, reject) => {
        assetManager.loadBundle(name, (error, _3rd) =>{
            if (error) {
                reject(error)
            } else {
                resolve(_3rd)
            }
        })
    })
    return promiseToLoadWasmModule
}

export function loadWasmModuleRvo (): Promise<void> {
    const errorReport = (msg: any): void => { error("loadWasmModuleRvo error: ", msg) }
    return loadSubpackage("3rd").then((_3rd: AssetManager.Bundle) => {
        if(sys.hasFeature(sys.Feature.WASM)) {
            const oTaskOptions = {
                bundle: '3rd',              // 包名
                preset: 'default',          //解析方式
                __outputAsArray__: false,   // 是否输出数组
                __requestType__: 'path',    // input的方式是数组
                __isNative__: true,         // 是否是原生资源(指的是资源本身
                ext: '.wasm'}               // 文件扩展名
            var task = new AssetManager.Task({input: 'rvo', options: oTaskOptions, onComplete: (err, RequestItem) => {
                if(err) {
                    return
                }
                const bufferAsset = RequestItem[0]
                initRvoWasm(rvoWasm, bufferAsset.url)
            }})
            assetManager.fetchPipeline.async(task) // 使用预载管线获得原生资源
            return
        }
        // todo: Load AsmJS
    }).catch(errorReport);
}

function initRvoWasm (wasmFactory: any, wasmUrl: string): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        const errorMessage = (err: any): string => `[Rvo]: Rvo wasm load failed: ${err}`
        wasmFactory({
            instantiateWasm (
                importObject: WebAssembly.Imports,
                receiveInstance: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void,
            ) {
                if(CCWebAssembly != undefined) {
                    CCWebAssembly.instantiate(wasmUrl, importObject).then((result)=>{
                        receiveInstance(result.instance, result.module)
                    })
                } else {
                    reject("CCWebAssembly is undefined!")
                }
            },
        }).then((Instance: any) => {
            const math = new Instance.Math()
            // 测试
            console.warn("Rvo wasm module loaded successfully. cal Math.add", math.add(1, 2)) // 测试部分
            math.hello()
            // 测试
        }).then(resolve).catch((err: any) => reject(errorMessage(err)))
    })
}

export interface ModuleConfigOptions {
    wasmBinary?: Uint8Array;
}

export default function rvoWasm(options?: ModuleConfigOptions): Promise<rvo>;

export interface rvo {
    
}

在游戏中调用 initRvoWasm

存在的问题

可能是性能检查终止了,实际上可以正常运行,不明确!

emmake ninja
make: ninja
[3/3] Linking CXX executable rvo.js
building:INFO: ctor_evaller: trying to eval global ctors (--ctors=__wasm_call_ctors)
building:INFO:

trying to eval __wasm_call_ctors
  ...partial evalling successful, but stopping since could not eval: call import: env._embind_register_class
  ...stopping