最近折腾了一下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