Skip to content

Support enums as a first-class entity #113

Description

@fcooper8472

Support enums as a first-class entity

Problem

There is no way to wrap a plain C++ enum. The only enums that reach Python are those that happen to be the sole member of a struct, and even then only as a side effect of the class writer.

Naming a bare enum in config.yaml is the natural thing to try, and it fails:

classes:
  - name: SemLatticeType # enum
    source_file: SemEnumerations.hpp
pygccxml.declarations.runtime_errors.declaration_not_found_t: Unable to find declaration.
  Matcher: [(decl type==class_t) and (name==SemLatticeType)]
pygccxml.declarations.runtime_errors.declaration_not_found_t: Unable to find declaration.
  Matcher: [(decl type==typedef_t) and (name==SemLatticeType)]

for

enum SemLatticeType : unsigned
{
    SEM_LATTICE_CUBIC,
    SEM_LATTICE_CLOSE_PACKED
};

Version 0.4.1 (1244f97).

Why it fails

Three things line up:

  1. A module config accepts only classes, free_functions and variables (parsers/package_info_parser.py:107-119), so an enum has to be declared as a class.
  2. CppClassInfo.update_decls() (info/class_info.py:174-188) resolves that name with source_ns.class_(), falling back to source_ns.typedef(). pygccxml exposes enums as a separate enumeration_t, and neither query matches one. parsers/source_parser.py:12-22 lists the declaration types cppwg handles, and enumeration_t is not among them.
  3. The struct-enum handling in writers/class_writer.py:276-296 is a special case inside the class writer: having found a class, it notices the class is a struct containing exactly one enum and emits py::enum_ rather than the usual class binding.
if type_traits_classes.is_struct(class_decl):
    enums = class_decl.enumerations(allow_empty=True)
    if len(enums) == 1:
        ...

So an enum is only reachable when nested inside something cppwg can already find. This is not a pybind11 limitation: py::enum_ works perfectly well at module scope.

Consequence for callers

The workaround is to restructure the C++ purely to satisfy the generator:

struct SemLatticeType
{
    enum Value : unsigned
    {
        SEM_LATTICE_CUBIC,
        SEM_LATTICE_CLOSE_PACKED
    };
};

Every use site then has to change: the type spelling becomes SemLatticeType::Value, and the values become SemLatticeType::SEM_LATTICE_CUBIC. In Chaste this was ~50 sites across 9 files, none of which improved the C++ (see Chaste/Chaste#111). RelativeTo in global/src/FileFinder.hpp has the same shape for the same reason.

Nor is skipping the config entry an option when the enum appears in a wrapped signature. writers/constructor_writer.py:180-222 emits default arguments as

py::arg("nodeLattice") = SemLatticeType::SEM_LATTICE_CUBIC

and pybind11 converts default arguments to Python objects at registration time, so an unregistered type raises on module import rather than at call time. Any enum used as a parameter type must be registered.

Proposal

Treat enums like free functions and variables, which already have this shape:

  • an enums: key in the module config, plus use_all_enums, alongside the existing three (parsers/package_info_parser.py:107-119)
  • info/enum_info.py, mirroring info/variable_info.py, resolving via source_ns.enumeration(name)scopedef_t.enumerations() is already used at class_writer.py:281, so the query is available
  • writers/enum_writer.py, mirroring writers/free_function_writer.py, emitting
py::enum_<Foo>(m, "Foo")
    .value("A", Foo::A)
    .export_values();
  • registration into module_writer.py next to the free-function and variable writers

Scoped enums (enum class) should work through the same path; .export_values() applies only to unscoped ones.

The existing struct-enum special case can stay as it is, so nothing currently wrapped changes behaviour. It would be worth a note in the README that a plain enum is now the recommended form and the struct wrapper is legacy.

Test case

enum Foo { A, B }; at namespace scope, wrapped via an enums: entry, callable from Python as Foo.A, and usable as a default argument of a wrapped function without raising at import.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions