C++和Rust通过wasmtime实现相互调用实例

1 wasmtime介绍

wasmtime是一个可以运行WebAssembly代码的运行时环境。

WebAssembly是一种可移植的二进制指令集格式,其本身与平台无关,类似于Java的class文件字节码。

WebAssembly本来的设计初衷是想让浏览器可以运行C语言这种编译型语言的代码。通常我们的C语言代码会使用gcc或clang等编译器直接编译链接成与平台相关的二进制可执行文件,这种与平台相关的二进制文件浏览器是无法直接运行的。如果想让浏览器运行C语言代码,就需要使用可将C语言编译成WebAssembly指令的编译器,编译好的代码是wasm格式。然后就可以使用各种wasm运行时来执行wasm代码,这就类似于JVM虚拟机执行class文件。

由于指令集和运行时环境本身与web场景并不绑定,因此随着后来的发展,WebAssembly指令集出现了可以脱离浏览器的独立运行时环境,WebAssembly的用途也变得更加广泛。

相比于浏览器的运行时,wasmtime是一个独立运行时环境,它可以脱离Web环境来执行wasm代码。它本身提供了命令行工具和API两种方式来执行wasm代码。本文主要介绍如何使用API方式来运行wasm代码。

2 wasmtime安装

2.1 wasmtime-cli安装

wasmtime-cli包含wasmtime命令,可以让我们直接在shell中运行wasm格式的代码。我们这里安装wasmtime主要是为了测试方便。

  1. 在shell中执行如下命令

    curl https://wasmtime.dev/install.sh -sSf | bash
  2. wasmtime的可执行文件会被安装在${HOME}/.wasmtime目录下

  3. 运行以上命令后会在${HOME}/.bashrc${HOME}/.bash_profile文件中帮我们添加以下环境变量

    export WASMTIME_HOME="${HOME}/.wasmtime"
    export PATH="$WASMTIME_HOME/bin:$PATH"
  4. 如果希望所有用户(包括root)可以使用wasmtime命令,可以将以上环境变量设置到/etc/profile.d中,我们可以在该目录下创建wasmtime.sh文件,并添加一下代码

    export WASMTIME_HOME=/home/<xxx>/.wasmtime  # 将xxx替换成自己的home目录
    export PATH="$WASMTIME_HOME/bin:$PATH"
  5. 可以使用如下命令直接运行wasm文件

    wasmtime hello.wasm

2.2 wasmtime库安装

如果想在代码中加载wasm文件并运行其中的代码,我们需要为我们使用的语言安装wasmtime库。注意这里的wasmtime库是为了让我们从代码中能够加载wasm文件并在wasmtime运行时中运行。wasmtime并不是wasm编译器,不能将C++或Rust代码编译成wasm文件,如果我们想将其他语言编译成wasm代码,需要下载各个语言自己的wasm编译器,具体安装方式在本文第3节。

目前wasmtime支持的语言有:

  • Rust
  • C
  • C++
  • Python
  • .NET
  • Go

我们这里以Rust和C++为例介绍如何安装wasmtime库

Rust

在Rust中使用wasmtime库非常简单,我们只需要在Cargo.toml配置文件中添加如下依赖

[dependencies]
wasmtime = "12.0.2"

C++

wasmtime的C++库需要我们引入wasmtime-cpp这个项目,wasmtime-cpp依赖wasmtime的C API,因此需要先安装C API。

  1. 可以在wasmtime的release中找到后缀为-c-api的包,比如我们安装的平台是x86_64-linux,那么我们可以下载如下文件

    wget https://github.com/bytecodealliance/wasmtime/releases/download/v12.0.2/wasmtime-v12.0.2-x86_64-linux-c-api.tar.xz
  2. 解压以上文件并将其移动到/usr/local目录下

    tar -xvf wasmtime-v12.0.2-x86_64-linux-c-api.tar.xz
    sudo mv ./wasmtime-v12.0.2-x86_64-linux-c-api /usr/local/wasmtime
  3. /etc/profile.d/wasmtime.sh中添加环境变量

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/wasmtime/lib
    export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/wasmtime/lib
    export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/local/wasmtime/include
    export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/local/wasmtime/include
  4. 下载wasmtime-cpp项目的include/wasmtime.hh文件,将其放到wasmtime.h所在的目录下,按照我们的安装步骤,需要放置到/usr/local/wasmtime/include目录下

  5. 如此就可以在我们的C++项目中引入wasmtime库了

    #include <wasmtime.hh>

3 wasm编译器安装

Rust

安装

Rust语言的编译器目前其实是一个LLVM的编译前端,它将代码编译成LLVM IR,然后经过LLVM编译成相应的目标平台代码。

因此我们并不需要替换Rust语言本身的编译器,只需要在编译时设置目标平台为wasm即可。我们在安装rust时,通常只会安装本机平台支持的目标,因此我们需要先安装wasm目标。

# 列出所有可安装的target列表
rustup target list

使用上面的命令后可以看到很多可以安装的target列表,其中已经安装的target后面会有(installed)标示。注意到其中有3个wasm相关的target。

wasm32-unknown-emscripten
wasm32-unknown-unknown
wasm32-wasi
  1. wasm32-unknown-emscripten:这个target是为了在Emscripten工具链下编译Wasm。Emscripten是一个将C/C++代码编译为Wasm和JavaScript的工具链。使用这个target,你可以在浏览器环境中运行编译后的Wasm代码。
  2. wasm32-unknown-unknown:这个target是为了在没有任何操作系统支持的情况下运行WebAssembly代码而设计的。这种情况下,WebAssembly代码将运行在一个“裸机”环境中,没有任何操作系统提供的支持。因此,如果你需要在裸机环境中运行WebAssembly代码,那么使用这个target是一个不错的选择。
  3. wasm32-wasi:这个target是为了在WebAssembly System Interface (WASI)上运行WebAssembly代码而设计的。WASI是一个标准接口,它提供了一些操作系统级别的功能,如文件系统和网络访问等。因此,如果你需要在WebAssembly中访问这些操作系统级别的功能,那么使用这个target是一个不错的选择。

由于我们不需要在Web环境中运行Rust代码,因此我们选择安装wasm32-unknown-unknownwasm32-wasi两个目标。运行以下两条指令,将这两个目标平台加入到当前使用的Rust工具链中。

rustup target add wasm32-unknown-unknown
rustup target add wasm32-wasi

使用

当我们需要将一个Rust项目编译成wasm时,可以选择执行如下的两种编译命令

# 在项目根目录执行
cargo build --target wasm32-unknown-unknown # 将在target/wasm32-unknown-unknown目录中生成build中间结果和wasm文件 # 或者执行
cargo build --target wasm32-wasi # 将在target/wasm32-wasi目录中生成build中间结果和wasm文件

C++

安装

目前,要将C++项目编译成WebAssembly,最常用的工具链是emscripten。emscripten支持将C,C++或任何使用了LLVM的语言编译成浏览器,Node.js或wasm运行时可以运行的代码。

Emscripten is a complete compiler toolchain to WebAssembly, using LLVM, with a special focus on speed, size, and the Web platform.

WebAssembly目前支持两种标准API:

  • Web APIs
  • WASI APIs

Emscripten对JavaScript API做了重构,将其包装在与WASI接口一样的API中,然后Emscripten在编译代码时,将尽可能的使用WASI APIs,以此来避免不必要的API差异。因此Emscripten编译出来的wasm文件大部分时候可以同时运行在Web和非Web环境中。

使用如下命令下载emsdk

git clone https://github.com/emscripten-core/emsdk.git

cd emsdk

使用如下命令安装最新的工具

git pull

./emsdk install latest

./emsdk activate latest

如果临时将emsdk的工具目录加入环境变量,可以运行

source ./emsdk_env.sh

或者可以在/etc/profile.d目录中创建emsdk.sh文件,并加入如下环境变量的配置,需要将<emsdk_installed_dir>替换为emsdk所在的目录。

export PATH=$PATH:<emsdk_installed_dir>/emsdk:<emsdk_installed_dir>/emsdk/node/16.20.0_64bit/bin:<emsdk_installed_dir>/emsdk/upstream/emscripten
export EMSDK=<emsdk_installed_dir>/emsdk
export EMSDK_NODE=<emsdk_installed_dir>/emsdk/node/16.20.0_64bit/bin/node

使用如下命令测试是否安装成功,如果输出下面的信息,说明我们已经可以正常使用emscripten的工具链。

> emcc -v

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.45 (ef3e4e3b044de98e1811546e0bc605c65d3412f4)
clang version 18.0.0 (https://github.com/llvm/llvm-project d1e685df45dc5944b43d2547d0138cd4a3ee4efe)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: <emsdk_installed_dir>/emsdk/upstream/bin

使用

由于我们不使用Web运行时,下面将只介绍将C或C++代码编译成独立wasm二进制文件的使用方法。

  1. 简单使用
emcc -O3 hello.cpp -o hello.wasm

当我们将输出目标的后缀名指定为wasm时,编译器会自动帮我们设置如下连接选项,上面的命令与下面的命令时等价的

emcc -O3 hello.cpp -o hello.wasm -s STANDALONE_WASM

这样编译出来的结果不会包含js文件,只会包含一个可被wasmtime运行的wasm文件。

  1. 结合cmake使用

更常用的方式通常是将整个C++项目编译成wasm,因此我们需要将工具链与cmake结合来构建整个项目。

假设我们有一个cmake项目有如下项目结构

hello_project
|-hello.cpp
|-CMakeLists.txt

其中hello.cpp中有如下代码

#include <stdio.h>

int main() {
printf("hello, world!\n");
return 0;
}

CMakeLists.txt应该按照下面的方式进行改写

cmake_minimum_required(VERSION 3.26)
project(hello_project) add_definitions(-std=c++17)
set(CMAKE_CXX_STANDARD 17) if (DEFINED EMSCRIPTEN)
add_executable(hello hello.cpp) set(CMAKE_EXECUTABLE_SUFFIX ".wasm") set_target_properties(foo PROPERTIES COMPILE_FLAGS "-Os")
set_target_properties(foo PROPERTIES LINK_FLAGS "-Os -s WASM=1 -s STANDALONE_WASM")
else()
add_executable(hello hello.cpp)
endif ()

以上CMakeLists.txt表示,当我们使用emscripten工具链进行编译时,将输出.wasm文件,且添加对应的编译和连接选项。当我们使用其他工具链编译时,将直接输出对应平台的可执行文件。

按照上面的方式写好CMakeLists.txt后,需要使用以下命令来执行编译的过程

# 在项目根目录下
mkdir build
cd build # 执行emcmake命令会帮我们自动配置cmake中指定的工具链为emscripten的工具链,这样就确保了使用的编译工具为emcc或em++,同时使用的标准库更改为emscripten提供的标准库
emcmake cmake ..
# 再执行make进行编译,编译后可以发现build目录中生成了hello.wasm文件
make

使用wasmtime-cli运行hello.wasm文件

> wasmtime hello.wasm

hello, world!

4 小试牛刀

实验场景

需要测试Rust代码被编译成wasm,C++代码被编译成wasm,在wasmtime中正确运行。其中C++代码可以调用Rust代码中的函数,然后外部可以调用C++代码中的函数。

  1. Rust项目:包含一个add函数,做两个整数的加法并返回结果,可以被外部调用。需要编译成wasm。
  2. C++项目:包含一个foo函数,调用Rust中的add函数并返回结果。需要编译成wasm。
  3. wasmtime项目:需要加载前面两个项目生成的wasm文件,并运行foo函数,看是否能获取正确的结果。

Rust项目编译成wasm

创建一个项目叫做demo-rust-wasmtime

cargo new demo-rust-wasmtime --lib

创建好的项目结构如下

demo-rust-wasmtime
├── Cargo.lock
├── Cargo.toml
└── src
   └── lib.rs

首先需要在Cargo.toml中配置生成的库为cdylib,这表示按照C语言的FFI来生成动态库,要想不同语言之间能够互相调用对方的函数,通常需要将不同的语言按照相同的FFI来进行编译,确保函数调用的方式是相同的。这里同时我们将Rust项目的名称修改为calc

[package]
name = "calc"
version = "0.1.0"
edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"] [dependencies]

lib.rs中实现我们需要的add函数

#[no_mangle]
pub extern "C" fn add(left: i32, right: i32) -> i32 {
left + right
}

这里有两个地方需要注意:

  • #[no_mangle]会通知Rust编译器,其后面的函数编译时名字不要进行混淆,确保使用add这个名称进行链接时能找到正确的函数。
  • extern "C"表示编译器需要确保函数在编译时使用与C语言相同的调用约定(ABI),从而使得函数可以与C语言代码无缝地进行交互,当然如果我们将不同的语言都遵照C语言的ABI进行编译,那么它们之间就可以互相调用。

C语言的调用约定规定了函数参数的传递方式、返回值的处理方式以及堆栈的清理方式。

这样就定义好了Rust项目中可以让外部使用的add方法。

我们使用如下命令对项目进行编译

cargo build --target wasm32-unknown-unknown
# 或
cargo build --target wasm32-wasi

这里两种target都可以使用,因为我们的项目中并没有使用任何系统的API,所以通常使用第一种target即可。

编译后可以在target/wasm-xxx/debug/目录下看到生成的calc.wasm文件。

可以使用wasmtime-cli实验一下是否能够调用add方法:

> wasmtime calc.wasm --invoke add 101 202

warning: using `--invoke` with a function that takes arguments is experimental and may break in the future
warning: using `--invoke` with a function that returns values is experimental and may break in the future
303

可以看到已经正确输出了结果,说明这个Rust项目已经被正确编译成了wasm。

C++项目编译成wasm

创建一个项目叫做demo-cpp-wasmtime,使用cmake作为构建工具,其目录结构如下

demo-cpp-wasmtime
├── CMakeLists.txt
├── toolbox.cpp
└── toolbox.h

正如第3节讲到的,我们需要使用emscripten工具链代替gcc工具链来将这个C++项目编译成wasm。

cmake配置

因此我们需要按照如下方式配置CMakeLists.txt文件

cmake_minimum_required(VERSION 3.26)
project(demo_cpp_wasmtime) add_definitions(-std=c++17)
set(CMAKE_CXX_STANDARD 17) if (DEFINED EMSCRIPTEN)
add_executable(toolbox toolbox.cpp toolbox.h) set(CMAKE_EXECUTABLE_SUFFIX ".wasm") set_target_properties(toolbox PROPERTIES COMPILE_FLAGS "-Os -s SIDE_MODULE=1")
set_target_properties(toolbox PROPERTIES LINK_FLAGS "-Os -s WASM=1 -s SIDE_MODULE=1 -s STANDALONE_WASM --no-entry")
else()
add_library(toolbox toolbox.cpp)
endif ()

这里有几点需要注意的

  1. 在使用emscripten时,我们使用add_executable指定编译目标为可执行文件,这是因为wasm本身是可执行的二进制代码,在没有特殊配置时,编译后的wasm代码中会生成一个_start函数,这个函数就是运行时执行wasm代码的入口。这里如果我们将add_executable替换成add_library,则使用emscripten编译后只会生成libtoolbox.a库文件,而不会生成wasm代码。

  2. 针对emscripten编译工具链,我们配置了编译参数和链接参数

    • -Os表示开启编译优化

    • -s SIDE_MODULE=1表示将toolbox编译成module,这样生成的wasm就类似动态链接库,可以让wasmtime在运行时动态链接这份wasm代码。

      emscripten支持将代码编译成两种不同的module

      1. Main modules:系统库会被链接进去
      2. Side modules:系统库不会被链接进去

      通常一个完整的项目只能有一个Main module,这个Main module可以链接多个Side module

      这里的编译选项SIDE_MODULE可以被设置为1或者2,设置成2则编译器会优化掉大量未被使用的代码或未被标记为EMSCRIPTEN_KEEPALIVE的代码,设置成1则会保留所有代码。

    • -s WASM=1表示只输出wasm文件,设置为0表示只输出js代码,设置成2表示两种代码都输出

    • -s STANDALONE_WASM表示编译的wasm是不依赖web环境而运行的

    • --no-entry编译生成的wasm代码通常需要有一个入口函数,也就是C++中需要有main函数,然而我们这里toolbox.cpp中将只有一个foo函数,因此我们需要使用这个链接参数来表示我们不需要入口函数。

代码实现

toolbox.h头文件如下

#pragma once

extern "C" {
int foo(int right);
}

类似Rust,这里我们声明了一个函数foo,并使用extern "C"表示这个foo函数需要按照C语言ABI进行编译。

接下来是toolbox.cpp的实现

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#define EM_IMPORT(NAME)
#endif extern "C" {
EM_IMPORT(add) int add(int a, int b);
} extern "C" {
EMSCRIPTEN_KEEPALIVE int foo(int right) {
return add(1, right);
}
}

下面解释一下代码中的几个宏的作用:

  • #ifdef __EMSCRIPTEN__:当我们使用emscripten工具链编译这个项目时,__EMSCRIPTEN__会被自动定义

  • EMSCRIPTEN_KEEPALIVEEM_IMPORT(NAME)

    这是头文件emscripten.h中定义的宏,查看源码可以发现

    #define EMSCRIPTEN_KEEPALIVE __attribute__((used))
    
    #ifdef __wasm__
    #define EM_IMPORT(NAME) __attribute__((import_module("env"), import_name(#NAME)))
    #else
    #define EM_IMPORT(NAME)
    #endif

    __attribute__((used))的作用是告诉编译器,即使该变量或函数没有被直接使用,也不要将其优化掉。这在一些特殊的情况下很有用,例如当你想要确保某个变量或函数在编译后的可执行文件中存在,即使它在代码中没有被显式调用或使用。这样就确保了我们的foo函数不会被编译器优化掉

    __attribute__((import_module("env"), import_name(#NAME)))是用于WebAssembly的特殊属性,用于指定导入函数所属的模块和导入函数的名称。在WebAssembly中,可以从外部导入函数,这些函数通常由宿主环境(如浏览器或wasmtime)提供。当你使用__attribute__((import_module("env"), import_name(#NAME)))属性时,它告诉WebAssembly运行时,该函数属于名为"env"的模块,并且其导入名称为#NAME

使用EM_IMPORT(add)宏告诉编译器,这里声明的add方法其具体实现来自于其他模块,具体就是来自于env模块中的add函数。因此这里声明的add方法其实可以起任意的名字,只要签名与env模块中的add方法相同即可。

编译

使用如下命令进行编译

# 在项目根目录下
mkdir build
cd build emcmake cmake ..
make

编译后在build目录下会生成toolbox.wasm二进制文件。

我们可以使用wasm2wat命令将编译好的wasm二进制文件转换成可读的wat文件来看一下生成的代码的结构

如果没有安装wasm2wat命令可以使用一下命令来安装

sudo apt install wabt

执行wasm2wat toolbox.wasm -o toolbox.wat命令后,可以打开toolbox.wat文件查看其结构如下

(module
(type (;0;) (func (param i32 i32) (result i32)))
(type (;1;) (func))
(type (;2;) (func (param i32) (result i32)))
(import "env" "add" (func (;0;) (type 0)))
(func (;1;) (type 1))
(func (;2;) (type 2) (param i32) (result i32)
i32.const 1
local.get 0
call 0)
(export "__wasm_call_ctors" (func 1))
(export "__wasm_apply_data_relocs" (func 1))
(export "foo" (func 2)))

可以看出,代码中import "env" "add"表示add函数来自env moduleadd函数。同时export "foo"表示toolbox.wasm对外暴露了foo函数。

wasmtime项目

wasmtime项目可以使用wasmtime支持的各种语言实现,这里我们以C++为例,看看如何将前面两个项目生成的.wasm文件调用起来。

创建一个项目叫做demo-run,使用cmake进行项目构建,其目录结构如下

demo-run
├── CMakeLists.txt
└── main.cpp

cmake配置

wasmtime项目可以使用gcc工具链进行编译,因此它的CMakeLists.txt可以正常进行配置

cmake_minimum_required(VERSION 3.26)
project(demo_run) set(CMAKE_CXX_STANDARD 17) add_executable(demo_run main.cpp)
target_link_libraries(demo_run PUBLIC wasmtime)

因为我们需要在代码中使用wasmtime的库,因此这里需要使用target_link_libraries(demo_run PUBLIC wasmtime)wasmtime链接进来。这也就要求必须先按照第2节中的安装方式配置好wasmtime的环境变量。

代码实现

具体wasmtime提供的每个API的用法在这里不多做赘述,具体可以参考wasmtime官方文档和官方提供的examples

#include <iostream>
#include <wasmtime.hh>
#include <fstream> using namespace wasmtime; std::vector<unsigned char> readFile(const char *name) {
std::ifstream watFile(name, std::ios::binary);
std::vector<unsigned char> arr;
char byte;
while (watFile.get(byte)) {
arr.push_back(byte);
}
return arr;
} int main() {
std::cout << "Compiling module" << std::endl;
Engine engine; // 加载calc.wasm成为module
auto calcByteArr = readFile("calc.wasm");
Span<uint8_t> calcSpan(calcByteArr.data(), calcByteArr.size());
auto calcModule = Module::compile(engine, calcSpan).unwrap(); // 加载toolbox.wasm成为module
auto toolboxByteArr = readFile("toolbox.wasm");
Span<uint8_t> toolboxSpan(toolboxByteArr.data(), toolboxByteArr.size());
auto toolboxModule = Module::compile(engine, toolboxSpan).unwrap(); std::cout << "Initializing..." << std::endl;
Store store(engine);
store.context().set_wasi(WasiConfig()).unwrap(); std::cout << "Linking..." << std::endl;
Linker linker(engine);
linker.define_wasi().unwrap(); // 链接器初始化calc module,实例化成具体的Instance
auto calcInst = linker.instantiate(store, calcModule).unwrap(); // 将上一步的calcInst中的所有export的对象定义到env module名下
linker.define_instance(store, "env", calcInst).unwrap(); // 链接器初始化toolbox module,实例化成具体的Instance
auto toolboxInst = linker.instantiate(store, toolboxModule).unwrap(); // 获取toolboxInst中的foo方法
auto func = std::get<Func>(toolboxInst.get(store, "foo").value()); // 调用foo方法,传入参数7,
auto fooRes = func.call(store, {7}).unwrap(); // 打印结果 FooResult: 8
std::cout << "FooResult: " << fooRes[0].i32() << std::endl; return 0;
}

就像注释中写的那样,我们将calc.wasmexport的方法add添加到了名称为envmodule下,这样上一步中C++编译成的.wasm代码就可以链接到这个add方法。

编译与运行

mkdir build
cd build
cmake ..
make

执行编译后会生成可执行文件demo_run,由于代码还要依赖两个.wasm文件,因此我们这里手动将前面两个项目生成的.wasm文件拷贝到demo_run可执行文件的同级目录下

运行生成的demo_run可执行文件后可得如下输出

> ./demo_run

Compiling module
Initializing...
Linking...
FooResult: 8

以上就实现了C++和Rust通过wasmtime实现相互调用的过程。

WebAssembly实践指南——C++和Rust通过wasmtime实现相互调用实例的更多相关文章

  1. Celery的实践指南

    http://www.cnblogs.com/ToDoToTry/p/5453149.html Celery的实践指南   Celery的实践指南 celery原理: celery实际上是实现了一个典 ...

  2. [CoreOS 转载] CoreOS实践指南(七):Docker容器管理服务

    转载:http://www.csdn.net/article/2015-02-11/2823925 摘要:当Docker还名不见经传的时候,CoreOS创始人Alex就预见了这个项目的价值,并将其做为 ...

  3. [CoreOS 转载] CoreOS实践指南(五):分布式数据存储Etcd(上)

    转载:http://www.csdn.net/article/2015-01-22/2823659 摘要:在“漫步云端:CoreOS实践指南”系列的前几篇,分别介绍了如何架设CoreOS集群,系统服务 ...

  4. [CoreOS 转载] CoreOS实践指南(四):集群的指挥所Fleet

    转载:http://www.csdn.net/article/2015-01-14/2823554/2 摘要:CoreOS是采用了高度精简的系统内核及外围定制的操作系统.ThoughtWorks的软件 ...

  5. OpenGL ES应用开发实践指南:iOS卷

    <OpenGL ES应用开发实践指南:iOS卷> 基本信息 原书名:Learning OpenGL ES for iOS:A Hands-On Guide to Modern 3D Gra ...

  6. 《赢在用户:Web人物角色创建和应用实践指南》阅读总结

           本书针对创建人物角色的每一个步骤,包括进行定性.定量的用户研究,生成人物角色分类,使人物角色真实可信等进行了十分详细的介绍.而且,在人物角色如何指导总体商业策略.确定信息架构.内容和设计 ...

  7. lua游戏开发实践指南学习笔记1

    本文是依据lua游戏开发实践指南做的一些学习笔记,仅用于继续自己学习的一些知识. Lua基础 1.  语言定义: 在lua语言中,标识符有非常大的灵活性(变量和函数名),只是用户不呢个以数字作为起始符 ...

  8. 《App架构实践指南》

    推荐书籍 <App 架构实践指南>

  9. Python 最佳实践指南 2018 学习笔记

    基础信息 版本 Python 2.7 Python 3.x Python2.7 版本在 2020 年后不再提供支持,建议新手使用 3.x 版本进行学习 实现 CPython:Python的标准实现: ...

  10. DevOps知识地图实践指南

    DevOps知识地图   DevOps方法论的主要来源是Agile, Lean 和TOC, 独创的方法论是持续交付. DevOps经典图书: * <DevOps实践指南> * <持续 ...

随机推荐

  1. YOLOV5实时检测屏幕

    YOLOV5实时检测屏幕 目录 YOLOV5实时检测屏幕 思考部分 先把原本的detect.py的代码贴在这里 分析代码并删减不用的部分 把屏幕的截图通过OpenCV进行显示 写一个屏幕截图的文件 用 ...

  2. 微型神经网络库MicroGrad-基于标量自动微分的类pytorch接口的深度学习框架

    一.MicroGrad MicroGrad是大牛Andrej Karpathy写的一个非常轻量级别的神经网络库(框架),其基本构成为一个90行python代码的标量反向传播(自动微分)引擎,以及在此基 ...

  3. 根据模板动态生成word(一)使用freemarker生成word

    @ 目录 一.准备模板 1.创建模板文件 2.处理模板 2.1 处理普通文本 2.2 处理表格 2.3 处理图片 二.项目代码 1.引入依赖 2.生成代码 三.验证生成word 一.准备模板 1.创建 ...

  4. 我开源了团队内部基于SpringBoot Web快速开发的API脚手架stater

    我们现在使用SpringBoot 做Web 开发已经比之前SprngMvc 那一套强大很多了. 但是 用SpringBoot Web 做API 开发还是不够简洁有一些. 每次Web API常用功能都需 ...

  5. [HUBUCTF 2022 新生赛]simple_RE

    [HUBUCTF 2022 新生赛]simple_RE 查壳,64位 找main函数,F5查看伪代码,简单分析一下 int __cdecl main(int argc, const char **ar ...

  6. 行行AI人才直播第12期:风平智能创始人林洪祥《AI数字人的技术实践和商业探讨》

    行行AI人才是博客园和顺顺智慧共同运营的AI行业人才全生命周期服务平台. 歌手孙燕姿凭借AI翻唱席卷各大视频平台.有视频博主用AI技术复活已故的奶奶,并且与之对话缅怀亲人填补遗憾.更有国外网红通过GP ...

  7. ChatGPT 1.0.0安卓分析,仅限国内分享

    ChatGPT 1.0.0安卓分析,仅限国内分享 博客园首发,本文将对ChatGpt Android版本1.0.0 APK进行静态解包分析和抓包分析,从ChatGpt Android APK功能的设计 ...

  8. 【go语言】2.2.1 数组和切片

    数组和切片是 Go 语言中常用的数据结构,它们都可以存储多个同类型的元素. 数组 数组是具有固定长度的数据类型,它的长度在定义时就已经确定,不能随意改变. 你可以使用以下方式定义一个数组: var a ...

  9. 如何用 Java 写一个 Java 虚拟机

    项目链接 https://github.com/FranzHaidnor/haidnorJVM haidnorJVM 使用 Java17 编写的 Java 虚拟机 意义 纸上得来终觉浅,绝知此事要躬行 ...

  10. Django: request.GET.get()

    释义 query = request.GET.get('name', '') 寻找名为name的GET参数,而且如果参数没有提交,返回一个空的字符串. 对比request.GET() 如果使用requ ...