Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[question] TypeScript binding of enums? #18585

Open
harsszegi opened this issue Jan 24, 2023 · 11 comments
Open

[question] TypeScript binding of enums? #18585

harsszegi opened this issue Jan 24, 2023 · 11 comments
Assignees
Labels

Comments

@harsszegi
Copy link

Hi,

I'm trying to wrap my head around how the enums are exposed with embind. Is there a documentation which provides a basic ".d.ts"-kinda description how is the ""class" structured, what interfaces it has? I'm having hard time to map them properly to ".d.ts", e.g. the mapped values don't behave like "normal" TypeScript enums (e.g. values mapped to numbers of strings).
Any idea?
Thanks,

@harichards
Copy link
Contributor

harichards commented Mar 30, 2023

You can do something like this:

my_enum.cc

enum OldStyle {
    OLD_STYLE_ONE,
    OLD_STYLE_TWO
};

enum class NewStyle {
    ONE,
    TWO
};

EMSCRIPTEN_BINDINGS(my_enum_example) {
    enum_<OldStyle>("OldStyle")
        .value("ONE", OLD_STYLE_ONE)
        .value("TWO", OLD_STYLE_TWO)
        ;

    enum_<NewStyle>("NewStyle")
        .value("ONE", NewStyle::ONE)
        .value("TWO", NewStyle::TWO)
        ;
}

my_enum.d.ts

export declare enum OldStyle {
    ONE = 0,
    TWO = 1,
}

export declare enum NewStyle {
    ONE = 0,
    TWO = 1,
}

export interface MyModule extends EmscriptenModule {
    OldStyle: typeof OldStyle
    NewStyle: typeof NewStyle
}

I learned this by examining the output of tsembind. EmscriptenModule comes from @types/emscripten. For free functions, classes, class methods and static class methods, see this comment and this comment.

@kirkwaiblinger
Copy link

Is that correct, though?

In my project I'm seeing enums come across the language barrier as objects with shape { value: number, constructor: Function }

enum<OldStyle>("OldStyle")
   .value("ONE", OLD_STYLE_ONE);   

becomes in JS

Module.OldStyle.ONE // { value: 0, constructor: f }

@mike-lischke
Copy link

I came here for the same reason. Enums are not represented as simple constants, but objects, so we cannot use them directly as values. However, switch statements work as expected. Yet, the documentation is not correct here!

@kripken kripken added the embind label Jun 20, 2023
@pwuertz
Copy link

pwuertz commented Apr 15, 2024

Are TypeScript-enums only available via tsembind?

With the build-in --embind-emit-tsd generator I'm getting non-enum .d.ts definitions like this:

export interface MyEnumValue<T extends number> {
  value: T;
}
export type MyEnum = MyEnumValue<0>|MyEnumValue<1>|MyEnumValue<2> ....

interface EmbindModule {
  MyEnum: {Value1: MyEnumValue<0>, Value2: MyEnumValue<1>, ...
  ...
}

instead of

export declare enum MyEnum {
  Value1 = 0,
  Value2 = 1,
  ...
}

@brendandahl
Copy link
Collaborator

The generated TS bindings for enums better matches how embind represents enums in JS. There's been talk of changing embind's enums into TS enums, but I haven't looked much into what the effects would be.

More discussion in #19387.

@MatthieuMv
Copy link

This non-constant representation is very annoying and there is currently no work around to generate typescript enums.
Is there any plan on developing such a feature or should I try to modify tooling code myself ?

Thank you

@brendandahl
Copy link
Collaborator

I'd like to look into at some point, but it's not on my immediate plans. I'd be happy to review a PR if you're interested in looking into it though.

@MatthieuMv
Copy link

@brendandahl

I ended up writing a python script that parses my C++ headers, and generate definitions for them.
Using EMSCRIPTEN_DECLARE_VAL_TYPE to generate a type, emscripten::register_type to register a generated enum and finally emscripten::internal::BindingType to register all C++ enums as numbers.

This is a bit hacky but this allows me to not register enumeration values one by one and potentially forget to update it when I change them.

@bansan85
Copy link

bansan85 commented Nov 5, 2024

@MatthieuMv It's cool that you found a solution. But I don't get it how to use mscripten::internal::BindingType.

Can you give a full small example ? For example, with an enum like:

enum E{A, B};

Thanks.

@MatthieuMv
Copy link

MatthieuMv commented Nov 5, 2024

Hello @bansan85 !

Here is a generic converter for any enum type. This registers and converts any enum from/to number.

    /** @brief Generate a typescript type for a given cpp type */
    template<typename Type>
    struct GenerateTypescriptType
    {
        EMSCRIPTEN_DECLARE_VAL_TYPE(TypescriptType);
    };

    /** @brief Register any enum binding type */
    template<typename EnumType>
        requires std::is_enum_v<EnumType>
    struct BindingType<EnumType> : public BindingType<val>
    {
        /** @brief Convert enum to wire */
        [[nodiscard]] static inline WireType toWireType(const EnumType value, rvp::default_tag tag) noexcept
        {
            return BindingType<val>::toWireType(
                GenerateTypescriptType<EnumType>::TypescriptType(val(std::to_underlying(value))),
                tag
            );
        }

        /** @brief Convert wire to enum */
        [[nodiscard]] static inline EnumType fromWireType(const WireType wire) noexcept
        {
            const auto value = BindingType<val>::fromWireType(wire);
            AssertRelease(value.isNumber(), "Invalid enum type");
            return EnumType(value.as<std::underlying_type_t<EnumType>>());
        }
    };

I still have to register the enum binding like that :

EMSCRIPTEN_BINDINGS(MyLibrary)
{
    // GroupCategory
    emscripten::register_type<MyEnum>("MyEnum");
}

Finally, here is the python script that parse enums of file list and store them into the Typescript binding file.

import re
import os

# Enumarate files to parse
scriptsDir = os.path.dirname(os.path.abspath(__file__))
filesToBind = [
    scriptsDir + "/../Path/To/CppFile/Enums.hpp",
    scriptsDir + "/../Path/To/CppFile/Enums2.hpp",
]

# Output file
outputPath = scriptsDir + "../Wasm/LiveClientTypes.d.ts"

# Parse enums from cpp file
def parseEnumsFromCppFile(filePath):
    """
    Parses C++ file for enumerations and stores them in a dictionary.

    Args:
    filePath (str): The path to the C++ header file.

    Returns:
    dict: A dictionary with enum names as keys and dictionaries of enum values as values.
    """
    enumDict = {}
    # enumPattern = re.compile(r"enum\s+class\s+(\w+)\s*(:\s*\w+\s*)?\{([^}]+)\};", re.DOTALL)
    enumPattern = re.compile(r"enum\s+class\s+(\w+)\s*(?::\s*[\w:]+\s*)?\{([^}]+)\};", re.DOTALL)

    # Open file
    with open(filePath, 'r') as file:
        # Read file
        content = file.read()
        # Remove single line comments
        content = re.sub(r"//.*", "", content)
        # Remove block comments
        content = re.sub(r"/\*.*?\*/", "", content)

    # Find all enums
    enums = enumPattern.findall(content)

    # Parse enums
    for enum in enums:
        enumName, enumValues = enum
        value_dict = {}
        # Split the enum values into individual items, remove spaces and empty lines
        entries = enumValues.split(',')
        baseValue = 0  # Default start value

        for entry in entries:
            entry = entry.strip()
            if entry:
                if '=' in entry:
                    key, value = entry.split('=')
                    key = key.strip()
                    value = int(value.strip(), 0)
                    baseValue = value
                else:
                    key = entry.strip()
                    value = baseValue
                value_dict[key] = value
                baseValue += 1

        enumDict[enumName] = value_dict

    return enumDict

# Generated bindings
output = ''

# Generate bindings for every file
for fileToBind in filesToBind:
    enums = parseEnumsFromCppFile(fileToBind)
    for enum in enums:
        output += 'export enum ' + enum + ' {\n'
        fields = enums[enum]
        for field in fields:
            value = fields[field]
            output += '  ' + field + ' = ' + str(value) + ',\n'
        output += '}\n\n'

# Read the existing content of the file
with open(outputPath, 'r', encoding='utf-8') as file:
    originalContent = file.read()

# Write the new text followed by the original content
with open(outputPath, 'w', encoding='utf-8') as file:
    file.write(output + originalContent)

Hope it helps !

@bansan85
Copy link

bansan85 commented Nov 5, 2024

Thanks a lot for your fast sharing. Yes, it helped me to understand your solution.

But it's not what I expected 😕 . You converted C++ type to Javascript number (Yes, that what you said but I didn't expected that Javascript will lose all other information). So you lose the type and the naming of each value in the enumerator.

On my side, I would like to keep the name of the C++ enum in the Javascript object.

See below a Firefox log of an class object AlignConsecutiveStyle and an enum object BreakBeforeNoexceptSpecifierStyle.
You can see that the class name AlignConsecutiveStyle is the name field of PtrType. But I can't get BreakBeforeNoexceptSpecifierStyle from the enum, except by hacking the name of the constructor.

image

I will still investigate and see if I use something like BindingType or another solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants