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.
Biggest change is that making
enumObject[Symbol.iterator]
non-enumerable, just like Arrays and usingObject.assign(Object.create(null), ...)
instead of directly modifying prototype.