From 604deb51cc16c19ce0111d391171ce5c4c8f88ae Mon Sep 17 00:00:00 2001 From: Stuart Dilts Date: Wed, 24 Jun 2026 17:11:51 -0600 Subject: [PATCH 1/4] Always use the enum type name in fields --- cl_bindgen/processfile.py | 36 ++++++++++++++++++++++------------- integrated/inputs/enums.h | 12 +++++++++++- integrated/outputs/enums.lisp | 20 ++++++++++++++++--- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/cl_bindgen/processfile.py b/cl_bindgen/processfile.py index ee085ac..8cc906e 100644 --- a/cl_bindgen/processfile.py +++ b/cl_bindgen/processfile.py @@ -151,18 +151,18 @@ def _explicitly_typed_enum_p(decl: clang.Cursor): return True return False -def _emit_enum_type(decl: clang.Cursor, options: ProcessOptions, location: clang.SourceLocation): - mangled_name = _mangle_string(decl.spelling, options.type_manglers) +def _emit_enum_type(decl: clang.Cursor, options: ProcessOptions, location: clang.SourceLocation, name=None): + if name is not None: + mangled_name = name + else: + mangled_name = _mangle_string(decl.spelling, options.type_manglers) # Because the default underlying enum type is dependent on the # system and the enum's value, we can't use the underlying type # and must use the enum as declared unless the underlying type # was explicity set: if _explicitly_typed_enum_p(decl): enum_type = _cursor_lisp_type_str(decl.enum_type, options, location) - if options.expand_enum_p(decl.spelling): - return f"{mangled_name} #| {enum_type} |#" - else: - return f"{enum_type} #| {mangled_name} |#" + return f"{mangled_name} #| {enum_type} |#" else: return f"{mangled_name}" @@ -172,8 +172,8 @@ def _determine_elaborated_field(field, inner_name, output, options, found_record actual_elaborated_type = _determine_elaborated_type(field.type) if actual_elaborated_type == _ElaboratedType.ENUM: decl = field.type.get_declaration() - _process_enum_as_constants(decl, output, options) - return _emit_enum_type(decl, options, field.location) + _process_realized_enum(inner_name, decl, output, options, as_constants=True) + return _emit_enum_type(decl, options, field.location, name=inner_name) elif actual_elaborated_type == _ElaboratedType.UNION: _process_record(inner_name, actual_elaborated_type, field, output, options, found_records) return "(:union " + inner_name + ")" @@ -245,7 +245,8 @@ def process_record_type(): if named_type_kind == TypeKind.RECORD: return process_record_type() elif named_type_kind == TypeKind.ENUM: - return f":int #| {_mangle_string(named_type.spelling, options.type_manglers)} |#" + enum_decl = type_obj.get_declaration() + return _emit_enum_type(enum_decl, options, location) elif named_type_kind == TypeKind.TYPEDEF: return _cursor_typedef_str(type_obj, options) elif kind == TypeKind.RECORD: @@ -405,7 +406,7 @@ def _process_union_decl(cursor, data: _ParseData, output, options): else: data.skipped_records[cursor.hash] = (_ElaboratedType.UNION, cursor) -def _process_realized_enum(name, cursor, output, options): +def _process_realized_enum(name, cursor, output, options, as_constants=False): if _explicitly_typed_enum_p(cursor): type_name = _cursor_lisp_type_str(cursor.enum_type, options, cursor.location) output.write(f"(cffi:defcenum ({name} {type_name})") @@ -414,7 +415,10 @@ def _process_realized_enum(name, cursor, output, options): _output_comment(cursor, output, before='\n',after='') for field in cursor.get_children(): - name = _mangle_string(field.spelling, options.enum_manglers) + if as_constants: + name = _mangle_string(field.spelling, options.constant_manglers) + else: + name = _mangle_string(field.spelling, options.enum_manglers) output.write(f"\n ({name} {field.enum_value})") output.write(")\n\n") @@ -428,10 +432,16 @@ def _process_enum_as_constants(cursor, output, options): def _process_enum_decl(cursor, data, output, options): name = cursor.spelling if name: - name = _mangle_string(name, options.type_manglers) - _process_realized_enum(name, cursor, output, options) + if cursor.is_anonymous(): + name = f'anon-enum-{_process_enum_decl.anon_count}' + _process_enum_decl.anon_count = _process_enum_decl.anon_count + 1 + _process_realized_enum(name, cursor, output, options, as_constants=True) + else: + name = _mangle_string(name, options.type_manglers) + _process_realized_enum(name, cursor, output, options, as_constants=False) else: data.skipped_enums[cursor.hash] = cursor +_process_enum_decl.anon_count = 0 def _use_string_ret_type(name, ret_type, options): kind = ret_type.kind diff --git a/integrated/inputs/enums.h b/integrated/inputs/enums.h index 597e597..94e5aed 100644 --- a/integrated/inputs/enums.h +++ b/integrated/inputs/enums.h @@ -4,6 +4,10 @@ enum test_enum { TEST_ENUM_FIVE = 5, }; +enum { + ANNON_ENUM_CONSTANT = 20 +}; + enum typed_enum : short { TYPED_ENUM_TEST }; @@ -11,8 +15,14 @@ enum typed_enum : short { struct outer { enum test_enum test_enum; enum typed_enum typed; - enum : short { + enum { OUTER_INNER_TEN = 10, OUTER_INNER_OTHER } inner_enum; }; + +typedef enum { TYPEDEF_ENUM_VAL } typedef_enum; + +typedef enum inner_typedef_enum { + INNER_TYPEDEF_VAL +} inner_typedef; diff --git a/integrated/outputs/enums.lisp b/integrated/outputs/enums.lisp index 5f2d787..fe3b0d8 100644 --- a/integrated/outputs/enums.lisp +++ b/integrated/outputs/enums.lisp @@ -5,13 +5,27 @@ (:test-enum-two 1) (:test-enum-five 5)) +(cffi:defcenum anon-enum-0 + (+annon-enum-constant+ 20)) + (cffi:defcenum (typed-enum :short) (:typed-enum-test 0)) -(defconstant +outer-inner-ten+ 10) -(defconstant +outer-inner-other+ 11) +(cffi:defcenum outer-inner-enum + (+outer-inner-ten+ 10) + (+outer-inner-other+ 11)) (cffi:defcstruct outer (test-enum test-enum) (typed :short #| typed-enum |#) - (inner-enum :short #| enum (unnamed at inputs/enums.h:14:3) |#)) + (inner-enum outer-inner-enum)) + +(cffi:defcenum typedef-enum + (:typedef-enum-val 0)) + +(cffi:defctype typedef-enum typedef-enum) + +(cffi:defcenum inner-typedef-enum + (:inner-typedef-val 0)) + +(cffi:defctype inner-typedef inner-typedef-enum) From e011e2951838fc508017dac329fcf5bab031cb28 Mon Sep 17 00:00:00 2001 From: Stuart Dilts Date: Wed, 24 Jun 2026 17:28:47 -0600 Subject: [PATCH 2/4] Remove enum-expansion option and replace with enum-constant option Allow enum values to be transformed into constants instead of keywords. --- README.md | 7 +++---- cl_bindgen/processfile.py | 15 ++++++++++++--- cl_bindgen/util.py | 4 ++-- integrated/outputs/enums.lisp | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 90e5ad3..d847fe6 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,9 @@ Optional Fields: `pkg-config --cflags` + `pointer-expansion` (experimental): Used to provide either a regex or a list of pointer types to expand or not expand in the output. -+ `enum-expansion`: By default, cl-bindgen does not expand enum types - into their converted lisp type, and instead converts them to their - underlying type. This allows you to specify which enums should get - expanded to their lisp type. ++ `enum-constants`: By default, cl-bindgen expands enum value + symbols into keywords. Types captured here will be expanded to + constants instead. + `make-inline` (experimental): Used to provide either a regex or a list of names matching functions that should be declared `inline`. diff --git a/cl_bindgen/processfile.py b/cl_bindgen/processfile.py index 8cc906e..c3e893e 100644 --- a/cl_bindgen/processfile.py +++ b/cl_bindgen/processfile.py @@ -79,7 +79,7 @@ class ProcessOptions: default_factory=lambda: lambda s: True) return_str_p: typing.Callable[[str], bool] = dataclasses.field( default_factory=lambda: lambda s: False) - expand_enum_p: typing.Callable[[str], bool] = dataclasses.field( + enum_constant_p: typing.Callable[[str], bool] = dataclasses.field( default_factory=lambda: lambda s: False) output: str = dataclasses.field(default_factory=lambda: ":stdout") @@ -414,11 +414,16 @@ def _process_realized_enum(name, cursor, output, options, as_constants=False): output.write(f"(cffi:defcenum {name}") _output_comment(cursor, output, before='\n',after='') + + if as_constants or options.enum_constant_p(name): + manglers = options.constant_manglers + else: + manglers = options.enum_manglers for field in cursor.get_children(): if as_constants: - name = _mangle_string(field.spelling, options.constant_manglers) + name = _mangle_string(field.spelling, manglers) else: - name = _mangle_string(field.spelling, options.enum_manglers) + name = _mangle_string(field.spelling, manglers) output.write(f"\n ({name} {field.enum_value})") output.write(")\n\n") @@ -433,6 +438,10 @@ def _process_enum_decl(cursor, data, output, options): name = cursor.spelling if name: if cursor.is_anonymous(): + # Although we could just emit a constant here, + # CFFI might need to do something to the definition + # because it's an enum. To be safe, emit that too. It won't + # affect the API at all. name = f'anon-enum-{_process_enum_decl.anon_count}' _process_enum_decl.anon_count = _process_enum_decl.anon_count + 1 _process_realized_enum(name, cursor, output, options, as_constants=True) diff --git a/cl_bindgen/util.py b/cl_bindgen/util.py index 873bb04..4fcfa78 100644 --- a/cl_bindgen/util.py +++ b/cl_bindgen/util.py @@ -63,7 +63,7 @@ def _process_batch_options(option, dictionary): force = dictionary.get('force') pkg_config = dictionary.get('pkg-config') ptr_handling = dictionary.get('pointer-expansion') - enum_handling = dictionary.get('enum-expansion') + enum_handling = dictionary.get('enum-constants') inline_handling = dictionary.get('make-inline') return_str = dictionary.get('string-return') if ptr_handling: @@ -77,7 +77,7 @@ def _process_batch_options(option, dictionary): )) option.declaim_inline_rules.extend(rules) if enum_handling: - option.expand_enum_p = process_inclusion_rules(enum_handling) + option.enum_constant_p = process_inclusion_rules(enum_handling) if return_str: option.return_str_p = process_inclusion_rules(return_str) if output: diff --git a/integrated/outputs/enums.lisp b/integrated/outputs/enums.lisp index fe3b0d8..b77b14d 100644 --- a/integrated/outputs/enums.lisp +++ b/integrated/outputs/enums.lisp @@ -17,7 +17,7 @@ (cffi:defcstruct outer (test-enum test-enum) - (typed :short #| typed-enum |#) + (typed typed-enum #| :short |#) (inner-enum outer-inner-enum)) (cffi:defcenum typedef-enum From 37faee559e379ddaf8737dba24697b90817e2ee7 Mon Sep 17 00:00:00 2001 From: Stuart Dilts Date: Wed, 24 Jun 2026 21:31:57 -0600 Subject: [PATCH 3/4] Fix constant arrays in function params We want to emit `:pointer` instead of the constant array information. --- cl_bindgen/processfile.py | 9 ++++++--- integrated/inputs/constant_array_in_param.h | 2 ++ integrated/outputs/constant-array-in-param.lisp | 5 +++++ integrated/run_tests.py | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 integrated/inputs/constant_array_in_param.h create mode 100644 integrated/outputs/constant-array-in-param.lisp diff --git a/cl_bindgen/processfile.py b/cl_bindgen/processfile.py index c3e893e..b4bce96 100644 --- a/cl_bindgen/processfile.py +++ b/cl_bindgen/processfile.py @@ -208,7 +208,7 @@ def _cursor_typedef_str(type_obj, options): else: return _mangle_string(type_decl_str, options.typedef_manglers) -def _cursor_lisp_type_str(type_obj, options, location=None): +def _cursor_lisp_type_str(type_obj, options, location=None, field=False): def process_record_type(): type_decl = type_obj.get_declaration() mangled_name = _mangle_string(type_decl.spelling, options.type_manglers) @@ -258,7 +258,10 @@ def process_record_type(): elem_type = type_obj.element_type num_elems = type_obj.element_count type_str = _cursor_lisp_type_str(elem_type, options, location) - return f"{type_str} :count {num_elems}" + if field: + return f"{type_str} :count {num_elems}" + else: + return f":pointer #| {type_str} :count {num_elems} |#" elif kind == TypeKind.FUNCTIONPROTO: return f":void #| {type_obj.spelling} |#" elif kind == TypeKind.FUNCTIONNOPROTO: @@ -360,7 +363,7 @@ def _extract_record_fields(name, cursor, text_stream, output, options, found_rec raise ProcessingError("Uknown typekind: " + str(field.type.kind), cursor.location) else: - field_type = _cursor_lisp_type_str(field.type, options, cursor.location) + field_type = _cursor_lisp_type_str(field.type, options, cursor.location, field=True) text_stream.write(f"\n ({field_name} {field_type})") def _process_record(name, actual_type, cursor, output, options, found_records: set): diff --git a/integrated/inputs/constant_array_in_param.h b/integrated/inputs/constant_array_in_param.h new file mode 100644 index 0000000..2cd8c35 --- /dev/null +++ b/integrated/inputs/constant_array_in_param.h @@ -0,0 +1,2 @@ + +void test(float args[4], int ints[10]); diff --git a/integrated/outputs/constant-array-in-param.lisp b/integrated/outputs/constant-array-in-param.lisp new file mode 100644 index 0000000..68981c5 --- /dev/null +++ b/integrated/outputs/constant-array-in-param.lisp @@ -0,0 +1,5 @@ +;; next section imported from file inputs/constant_array_in_param.h + +(cffi:defcfun "test" :void + (args :pointer #| :float :count 4 |#) + (ints :pointer #| :int :count 10 |#)) diff --git a/integrated/run_tests.py b/integrated/run_tests.py index c72872e..45644b5 100644 --- a/integrated/run_tests.py +++ b/integrated/run_tests.py @@ -35,6 +35,7 @@ def gen_fn(inputfile, outputfile): ('inputs/capitalization.h', 'outputs/capitalization.lisp', {}), ('inputs/multiple_forward_decls.h', 'outputs/multiple_forward_decls.lisp', {}), ('inputs/enums.h', 'outputs/enums.lisp', {}), + ('inputs/constant_array_in_param.h', 'outputs/constant-array-in-param.lisp', {}), ] def test_file_generation(): From 368219cb8987a7f2aac0f7938fb1f648dc811b81 Mon Sep 17 00:00:00 2001 From: Stuart Dilts Date: Wed, 24 Jun 2026 21:34:29 -0600 Subject: [PATCH 4/4] Make exclude list override include match --- cl_bindgen/inclusion_rules.py | 17 ++++++++++++++--- test/test_inclusion_rules.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/cl_bindgen/inclusion_rules.py b/cl_bindgen/inclusion_rules.py index ef5edc9..4cffca5 100644 --- a/cl_bindgen/inclusion_rules.py +++ b/cl_bindgen/inclusion_rules.py @@ -66,15 +66,26 @@ def make_batch_determiner(whitelist=None, blacklist=None, include_matcher=None, includer = _compile_regexes(include_matcher) def fn(typename): in_white = (typename in allowed_set or _match_regex_list(includer, typename)) - not_black = (not _match_regex_list(excluder, typename)) + not_black = not (_match_regex_list(excluder, typename) or + typename in blacklist) return in_white and not_black return fn elif exclude_matcher is not None: excluder = _compile_regexes(exclude_matcher) - return lambda x: x in allowed_set and not _match_regex_list(excluder, x) + def fn(typename): + in_white = typename in allowed_set + not_black = not (_match_regex_list(excluder, typename) or + typename in blacklist) + return in_white and not_black + return fn elif include_matcher: includer = _compile_regexes(include_matcher) - return lambda x: x in allowed_set or _match_regex_list(includer, x) + def fn(typename): + in_white = (typename in allowed_set + or _match_regex_list(includer, typename)) + not_black = typename not in blacklist + return in_white and not_black + return fn else: return lambda x: x in allowed_set else: diff --git a/test/test_inclusion_rules.py b/test/test_inclusion_rules.py index 970ee32..bbae897 100644 --- a/test/test_inclusion_rules.py +++ b/test/test_inclusion_rules.py @@ -61,3 +61,18 @@ def test_whitelist_blacklist_list_works(self): actual_allowed = ['cheese', 'curds'] self.assertTrue(all([ result(i) for i in actual_allowed])) self.assertFalse(result('fish')) + + def test_exclude_list_overrides_include_match(self): + banned = ['asdf', 'fdsa'] + rules = { + 'exclude': { + 'names': banned + }, + 'include': { + 'match': '.*' + } + } + result = process_inclusion_rules(rules, 'names') + self.assertTrue(all([ not result(i) for i in banned])) + actual_allowed = ['cheese', 'curds'] + self.assertTrue(all([ result(i) for i in actual_allowed]))