При работе с 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>