Skip to content

Instantly share code, notes, and snippets.

@jeremy-code
Last active May 7, 2025 01:56
Show Gist options
  • Save jeremy-code/c54f19402456466c4cf79b2bc7d0af96 to your computer and use it in GitHub Desktop.
Save jeremy-code/c54f19402456466c4cf79b2bc7d0af96 to your computer and use it in GitHub Desktop.
Emscripten Embind Enums

Suppose you are wanting to use Emscripten's Embind with an Enum.

// exif-byte-order.h
typedef enum {
	EXIF_BYTE_ORDER_MOTOROLA,
	EXIF_BYTE_ORDER_INTEL
} ExifByteOrder;
// .cpp
#include <libexif/exif-byte-order.h>
#include <emscripten/bind.h>

using namespace emscripten;

EMSCRIPTEN_BINDINGS(Enum) {
    enum_<ExifByteOrder>("ExifByteOrder")
        .value("MOTOROLA", EXIF_BYTE_ORDER_MOTOROLA)
        .value("INTEL", EXIF_BYTE_ORDER_INTEL)
        ;
}

This would be the resulting declaration TypeScript file.

// module.d.ts
export interface ExifByteOrderValue<T extends number> {
  value: T;
}
export type ExifByteOrder = ExifByteOrderValue<0>|ExifByteOrderValue<1>;

interface EmbindModule {
  ExifByteOrder: {MOTOROLA: ExifByteOrderValue<0>, INTEL: ExifByteOrderValue<1>};
}

So far so good, let's check what the console tells us:

console.log("ExifByteOrder", ExifByteOrder);
console.log("typeof ExifByteOrder", typeof ExifByteOrder);
console.log("Object.keys(ExifByteOrder)", Object.keys(ExifByteOrder));
console.log("ExifByteOrder['MOTOROLA'].value", ExifByteOrder["MOTOROLA"].value);
console.log("ExifByteOrder['INTEL'].name", ExifByteOrder["INTEL"].value);
node index.js
ExifByteOrder [Function: ctor] {
  values: { '0': ctor {}, '1': ctor {} },
  argCount: undefined,
  MOTOROLA: ctor {},
  INTEL: ctor {}
}
typeof ExifByteOrder function
Object.keys(ExifByteOrder) [ 'values', 'argCount', 'MOTOROLA', 'INTEL' ]
ExifByteOrder['MOTOROLA'].value 0
ExifByteOrder['INTEL'].name 1

...huh? I have no idea why it's a function (long shot guess, it's because WASM can only export functions and WebAssembly.Memory) but judging by the name "ctor", I'd guess it has something to do with the EVAL_CTORS option. Moreover, the argCount and values property do intrigue me.

Since it is a function, let's see how it reacts to typical function behavior (despite TypeScript's insisitence on it not existing)

console.log("ExifByteOrder.length", ExifByteOrder.length);
console.log("ExifByteOrder.name", ExifByteOrder.name);
console.log("ExifByteOrder.prototype", ExifByteOrder.prototype);
console.log("ExifByteOrder()", ExifByteOrder());
console.log("ExifByteOrder.toString()", ExifByteOrder.toString());
node index.js
ExifByteOrder.length 0
ExifByteOrder.name ctor
ExifByteOrder.prototype {}
ExifByteOrder() undefined
ExifByteOrder.toString() function ctor() {}

I'm going to assume at this point the pattern for the JavaScript is obvious and only show the output:

node index.js
ExifByteOrder.values { '0': ctor {}, '1': ctor {} }
typeof ExifByteOrder.values object
ExifByteOrder.values['1'] ctor {}
typeof ExifByteOrder.values['1'] object
ExifByteOrder['MOTOROLA'] ctor {}
typeof ExifByteOrder['MOTOROLA'] object
ExifByteOrder['INTEL'] ctor {}
typeof ExifByteOrder['INTEL'] object

Interesting how they're all objects. Let's see what values they have.

node index.js
Object.entries(ExifByteOrder.values) [ [ '0', ctor {} ], [ '1', ctor {} ] ]
Object.entries(ExifByteOrder.values['1']) []
Object.entries(ExifByteOrder['MOTOROLA']) []
Object.entries(ExifByteOrder['INTEL']) []

Ok, so nothing, perhaps it has non-enumerable properties?

node index.js
Object.getOwnPropertyDescriptors(ExifByteOrder.values) {
  '0': {
    value: ctor {},
    writable: true,
    enumerable: true,
    configurable: true
  },
  '1': {
    value: ctor {},
    writable: true,
    enumerable: true,
    configurable: true
  }
}
Object.getOwnPropertyDescriptors(ExifByteOrder.values['1']) {
  value: { value: 1, writable: false, enumerable: false, configurable: false },
  constructor: {
    value: [Function: ExifByteOrder_INTEL],
    writable: false,
    enumerable: false,
    configurable: false
  }
}
Object.getOwnPropertyDescriptors(ExifByteOrder['MOTOROLA']) {
  value: { value: 0, writable: false, enumerable: false, configurable: false },
  constructor: {
    value: [Function: ExifByteOrder_MOTOROLA],
    writable: false,
    enumerable: false,
    configurable: false
  }
}
Object.getOwnPropertyDescriptors(ExifByteOrder['INTEL']) {
  value: { value: 1, writable: false, enumerable: false, configurable: false },
  constructor: {
    value: [Function: ExifByteOrder_INTEL],
    writable: false,
    enumerable: false,
    configurable: false
  }
}

Well... neat. I have no idea what this means. But anyway, if you're looking for code to make this look more like a reasonable enum or object (though you lose the reverse mapping which might be what the .values is for...? I should've tested it with an enum that had better values)

/**
 * Before the C23 standard, enums were not allowed to have a value that was not
 * an `int`
 *
 * @see {@link https://open-std.org/JTC1/SC22/WG14/www/docs/n3029.htm}
 */
interface EmbindEnumValue<T> {
  value: T;
}

/**
 * Maps an enum generated from Emscripten's Embind to a plain object with the
 * key-value pairs from the enum
 *
 * @see {@link https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#enums}
 * @see {@link https://github.com/emscripten-core/emscripten/blob/main/src/lib/libembind_gen.js#L277-L310}
 */
const mapEmbindEnumToObject = <
  Enum extends Record<PropertyKey, EmbindEnumValue<unknown>>,
>(
  embindEnum: Enum,
) =>
  Object.fromEntries(
    Object.entries(embindEnum)
      .filter(([key]) => !["values", "argCount"].includes(key))
      .map(([key, value]) => [key, value.value]),
  ) as {
    [K in keyof Enum]: Enum[K] extends { value: infer V } ? V : never;
  };

export { mapEmbindEnumToObject };

I am really not a fan of the cast but there's really no way around it using Object.fromEntries(). If you wanted runtime error handling you probably should check if typeof enumObj === 'function'.

I don't know if this behavior is expected or will change in the future. Currently the docs only have two sentences for Enums, those being

Embind’s enumeration support works with both C++98 enums and C++11 “enum classes”. In both cases, JavaScript accesses enumeration values as properties of the type.

@jeremy-code
Copy link
Author

import type { EmbindEnum, EmbindEnumValue } from "../interfaces.ts";

type EmbindEnumObjectIterator<T extends EmbindEnum> = {
  readonly [Symbol.iterator]: () => ArrayIterator<
    [keyof T, T[keyof T]["value"]]
  >;
};

type EmbindEnumObject<T extends EmbindEnum> = {
  readonly [Key in keyof T]: T[Key] extends EmbindEnumValue<infer Value> ? Value
  : never;
} & EmbindEnumObjectIterator<T>;

/**
 * Maps an enum generated from Emscripten's Embind to a plain object with the
 * key-value pairs from the the enum's keys `.value` property. Adheres to the
 * ECMAScript enum proposal.
 *
 * @see {@link https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#enums}
 * @see {@link https://github.com/emscripten-core/emscripten/blob/main/src/lib/libembind_gen.js#L277-L310}
 * @see {@link https://github.com/tc39/proposal-enum}
 */
const mapEmbindEnumToObject = <Enum extends EmbindEnum>(
  embindEnum: Enum,
): EmbindEnumObject<Enum> => {
  const entries = Object.entries(embindEnum)
    .filter(([key]) => key !== "values" && key !== "argCount")
    .map<[string, unknown]>(([key, value]) => [key, value.value]);

  const enumObject = Object.assign(
    Object.create(null),
    Object.fromEntries(entries),
  );

  Object.defineProperty(enumObject, Symbol.iterator, {
    value: entries[Symbol.iterator].bind(entries),
    enumerable: false,
  });
  Object.preventExtensions(enumObject);

  return enumObject;
};

export { type EmbindEnumObject, mapEmbindEnumToObject };

Biggest change is that making enumObject[Symbol.iterator] non-enumerable, just like Arrays and using Object.assign(Object.create(null), ...) instead of directly modifying prototype.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment