При работе с JavaScript и взаимодействии с кодом C (FFI, NAPI, WASM) вы можете столкнуться со структурами C. Они универсальны, просты и суперполезны, и не только для C (вы можете использовать их с Rust и всем, что компилируется в Wasm, и многим другим).
Ранее я написал похожее объяснение строк C, и вы можете использовать очень простую/легкую библиотеку для взаимодействия с обеими, если хотите.
Простой пример
Допустим, у вас есть такой код:
// blend.c typedef struct Color { unsigned char r; unsigned char g; unsigned char b; unsigned char a; } Color; void blendColors(Color* c1, Color* c2, Color* ret) { ret->r = ((c1->r) / 2) + ((c2->r) / 2); ret->g = ((c1->g) / 2) + ((c2->g) / 2); ret->b = ((c1->b) / 2) + ((c2->b) / 2); ret->a = ((c1->a) / 2) + ((c2->a) / 2); }
Идея состоит в том, что вы даете ему два указателя цвета, и он смешивает их в возвращаемый цвет, вставляя значения в указатель.
Если бы вы собирались использовать его в обычной программе на C (это не требуется для WASM или DLL, просто отдельная программа), это выглядело бы примерно так:
// for printf #include <stdio.h> int main() { Color red = { .r = 255, .g = 0, .b = 0, .a = 255 }; Color blue = { .r = 0, .g = 0, .b = 255, .a = 255 }; Color purple = { .r = 0, .g = 0, .b = 0, .a = 0 }; blendColors(&red, &blue, &purple); printf("Blending (%hhu,%hhu,%hhu,%hhu) + (%hhu,%hhu,%hhu,%hhu) = (%hhu,%hhu,%hhu,%hhu)\n", red.r, red.g, red.b, red.a, blue.r, blue.g, blue.b, blue.a, purple.r, purple.g, purple.b, purple.a ); return 0; }
Это немного глупый пример, но он показывает, как передавать указатели на эти структуры.
Что такое указатель?
Я немного говорил об этом раньше, но указатель — это целое число, представляющее адрес некоторой памяти. В 64-битных системах (например, в современных родных) это u64
, а в 32-битных системах (например, WASM) — u32
. В WASM вы можете передавать только простые числовые типы (i32
, u32
, i64
, u64
, f32
, f64)
, поэтому остальные элементы необходимо передавать в виде указателей (u32
).
Что такое структуры C?
По сути, это просто коллекция воспоминаний. Каждое поле в структуре хранится рядом с другими полями в байтах. Итак, байты выглядят так:
struct Color { unsigned char r; unsigned char g; unsigned char b; unsigned char a; }; struct Color c = { .r=255, .g=255, .b=0, .a=255 }; // [255, 255, 0, 255]
В этом примере каждое из этих полей занимает байт. Вот пример структуры с большим количеством байтов (поскольку i32
занимает 4 байта):
struct Vector3 { int x; int y; int z; } struct Vector3 v = { .x=0 , .y=10, .z=100 }; // [0,0,0,0, 10,0,0,0, 100,0,0,0]
Это «обратно», потому что это прямой порядок байтов, но каждый i32
занимает 4 байта. В случае со знаковыми типами отрицательные числа «зацикливаются», поэтому, возможно, за ними не так легко следить, но если вы это знаете, это имеет смысл:
struct Vector3 v = { .x=0 , .y=10, .z=-100 }; // [0,0,0,0, 10,0,0,0, 156,255,255,255]
Как мне скомпилировать это в WASM?
Это примерно то же самое, что и раньше:
# mount the current directory inside the docker and give me a bash-prompt docker run -it --rm -v $(pwd):/cart konsumer/null0:latest # now you are inside the container # compile blend.c, using wasi-sdk clang --sysroot=$WASI_SYSROOT -Wl,--export=blendColors -Wl,--export=free -Wl,--export=malloc -Wl,--no-entry -nostartfiles -o blend.wasm blend.c # inspect the wasm wasm-objdump -x blend.wasm
Как мне использовать это в JavaScript?
Это немного сложнее, чем другие типы, но все же довольно просто. Есть несколько способов, но мне нравится DataView. Он прекрасно работает со всеми типами, которые изначально поддерживает WASM, и его можно довольно легко использовать для разрешения других типов. Поместите это в файл .html:
<script type="module"> // standard wasm-stuff const { instance } = await WebAssembly.instantiateStreaming(fetch("blend.wasm"), {env: {}}) const { blendColors, malloc, free, memory } = instance.exports const mem = new DataView(memory.buffer) class Color { constructor(initialValue={}, address) { this._size = 4 this._address = address || malloc(this._size) for (const i of Object.keys(initialValue)) { this[i] = initialValue[i] } } get r() { return mem.getUint8(this._address) } get g() { return mem.getUint8(this._address + 1) } get b() { return mem.getUint8(this._address + 2) } get a() { return mem.getUint8(this._address + 3) } set r(v){ mem.setUint8(this._address, v) } set g(v){ mem.setUint8(this._address + 1, v) } set b(v){ mem.setUint8(this._address + 2, v) } set a(v){ mem.setUint8(this._address + 3, v) } } const red = new Color({ r: 255, g: 0, b: 0, a:255 }) const blue = new Color({ r: 0, g: 0, b: 255, a:255 }) const purple = new Color() blendColors(red._address, blue._address, purple._address) console.log(`${purple.r}, ${purple.g}, ${purple.b}, ${purple.a}`) </script>
malloc
создает указатели для использования, а затем мы напрямую манипулируем этой памятью. class
— небольшой помощник, делающий его более читабельным. Это достаточно просто, но если вам нужно работать с большим количеством структур, это может быть немного затруднительно.
Примечание: если вы console.log
несколько байтов в Chrome, у вас появится хороший шестнадцатеричный редактор для просмотра:
console.log(memory.buffer.slice(purple._address, purple._address + purple._size))
Вот пример использования my lib, который по сути делает то же самое, но гораздо проще:
<script type=module> import memhelpers from 'https://cdn.jsdelivr.net/npm/cmem_helpers/+esm' const { instance } = await WebAssembly.instantiateStreaming(fetch("blend.wasm"), {env: {}}) const { blendColors, malloc, free, memory } = instance.exports const { struct } = memhelpers(memory.buffer, malloc) // this replaces the class, much simpler: const Color = struct({ r: 'Uint8', g: 'Uint8', b: 'Uint8', a: 'Uint8' }) const red = Color({ r: 255, g: 0, b: 0, a:255 }) const blue = Color({ r: 0, g: 0, b: 255, a:255 }) const purple = Color() blendColors(red._address, blue._address, purple._address) console.log(`${purple.r}, ${purple.g}, ${purple.b}, ${purple.a}`) </script>