ruby-bindgen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE +25 -0
- data/README.md +68 -0
- data/Rakefile +8 -0
- data/bin/ruby-bindgen +133 -0
- data/docs/architecture.md +238 -0
- data/docs/c/c_bindings.md +65 -0
- data/docs/c/constants.md +21 -0
- data/docs/c/customizing.md +19 -0
- data/docs/c/filtering.md +24 -0
- data/docs/c/getting_started.md +56 -0
- data/docs/c/library_loading.md +53 -0
- data/docs/c/output.md +96 -0
- data/docs/c/types.md +61 -0
- data/docs/c/version_guards.md +105 -0
- data/docs/cmake/cmake_bindings.md +19 -0
- data/docs/cmake/filtering.md +26 -0
- data/docs/cmake/getting_started.md +52 -0
- data/docs/cmake/output.md +110 -0
- data/docs/configuration.md +351 -0
- data/docs/contributing.md +68 -0
- data/docs/cpp/buffers.md +24 -0
- data/docs/cpp/classes.md +139 -0
- data/docs/cpp/cpp_bindings.md +29 -0
- data/docs/cpp/customizing.md +124 -0
- data/docs/cpp/enums.md +42 -0
- data/docs/cpp/filtering.md +35 -0
- data/docs/cpp/getting_started.md +80 -0
- data/docs/cpp/iterators.md +94 -0
- data/docs/cpp/operators.md +170 -0
- data/docs/cpp/output.md +125 -0
- data/docs/cpp/templates.md +114 -0
- data/docs/examples.md +133 -0
- data/docs/index.md +133 -0
- data/docs/prior_art.md +37 -0
- data/docs/troubleshooting.md +243 -0
- data/docs/type_spelling.md +200 -0
- data/docs/updating_bindings.md +55 -0
- data/docs/version_guards.md +69 -0
- data/lib/ruby-bindgen/config.rb +63 -0
- data/lib/ruby-bindgen/generators/cmake/cmake.rb +171 -0
- data/lib/ruby-bindgen/generators/cmake/directory.erb +29 -0
- data/lib/ruby-bindgen/generators/cmake/guard.rb +55 -0
- data/lib/ruby-bindgen/generators/cmake/presets.erb +232 -0
- data/lib/ruby-bindgen/generators/cmake/project.erb +89 -0
- data/lib/ruby-bindgen/generators/ffi/callback.erb +1 -0
- data/lib/ruby-bindgen/generators/ffi/constant.erb +1 -0
- data/lib/ruby-bindgen/generators/ffi/enum_constant_decl.erb +1 -0
- data/lib/ruby-bindgen/generators/ffi/enum_decl.erb +4 -0
- data/lib/ruby-bindgen/generators/ffi/enum_decl_anonymous.erb +4 -0
- data/lib/ruby-bindgen/generators/ffi/ffi.rb +687 -0
- data/lib/ruby-bindgen/generators/ffi/field_decl.erb +1 -0
- data/lib/ruby-bindgen/generators/ffi/function.erb +5 -0
- data/lib/ruby-bindgen/generators/ffi/library.erb +39 -0
- data/lib/ruby-bindgen/generators/ffi/project.erb +18 -0
- data/lib/ruby-bindgen/generators/ffi/struct.erb +6 -0
- data/lib/ruby-bindgen/generators/ffi/translation_unit.erb +9 -0
- data/lib/ruby-bindgen/generators/ffi/typedef_decl.erb +1 -0
- data/lib/ruby-bindgen/generators/ffi/union.erb +6 -0
- data/lib/ruby-bindgen/generators/ffi/variable.erb +1 -0
- data/lib/ruby-bindgen/generators/ffi/version.erb +9 -0
- data/lib/ruby-bindgen/generators/ffi/version_method.erb +5 -0
- data/lib/ruby-bindgen/generators/generator.rb +52 -0
- data/lib/ruby-bindgen/generators/rice/auto_generated_base_class.erb +5 -0
- data/lib/ruby-bindgen/generators/rice/class.erb +31 -0
- data/lib/ruby-bindgen/generators/rice/class_template.erb +9 -0
- data/lib/ruby-bindgen/generators/rice/class_template_specialization.erb +10 -0
- data/lib/ruby-bindgen/generators/rice/constant.erb +9 -0
- data/lib/ruby-bindgen/generators/rice/constructor.erb +1 -0
- data/lib/ruby-bindgen/generators/rice/conversion_function.erb +4 -0
- data/lib/ruby-bindgen/generators/rice/cxx_iterator_method.erb +1 -0
- data/lib/ruby-bindgen/generators/rice/cxx_method.erb +6 -0
- data/lib/ruby-bindgen/generators/rice/enum_constant_decl.erb +7 -0
- data/lib/ruby-bindgen/generators/rice/enum_decl.erb +6 -0
- data/lib/ruby-bindgen/generators/rice/field_decl.erb +8 -0
- data/lib/ruby-bindgen/generators/rice/function.erb +6 -0
- data/lib/ruby-bindgen/generators/rice/function_pointer.rb +68 -0
- data/lib/ruby-bindgen/generators/rice/incomplete_class.erb +3 -0
- data/lib/ruby-bindgen/generators/rice/iterator_alias.erb +1 -0
- data/lib/ruby-bindgen/generators/rice/iterator_collector.rb +159 -0
- data/lib/ruby-bindgen/generators/rice/namespace.erb +5 -0
- data/lib/ruby-bindgen/generators/rice/non_member_operator_binary.erb +4 -0
- data/lib/ruby-bindgen/generators/rice/non_member_operator_inspect.erb +6 -0
- data/lib/ruby-bindgen/generators/rice/non_member_operator_unary.erb +4 -0
- data/lib/ruby-bindgen/generators/rice/operator[].erb +4 -0
- data/lib/ruby-bindgen/generators/rice/project.cpp.erb +18 -0
- data/lib/ruby-bindgen/generators/rice/project.hpp.erb +13 -0
- data/lib/ruby-bindgen/generators/rice/reference_qualifier.rb +495 -0
- data/lib/ruby-bindgen/generators/rice/rice.rb +1724 -0
- data/lib/ruby-bindgen/generators/rice/rice_include.hpp.erb +7 -0
- data/lib/ruby-bindgen/generators/rice/signature_builder.rb +230 -0
- data/lib/ruby-bindgen/generators/rice/template_resolver.rb +585 -0
- data/lib/ruby-bindgen/generators/rice/translation_unit.cpp.erb +40 -0
- data/lib/ruby-bindgen/generators/rice/translation_unit.hpp.erb +7 -0
- data/lib/ruby-bindgen/generators/rice/translation_unit.ipp.erb +3 -0
- data/lib/ruby-bindgen/generators/rice/type_index.rb +117 -0
- data/lib/ruby-bindgen/generators/rice/type_speller.rb +509 -0
- data/lib/ruby-bindgen/generators/rice/union.erb +5 -0
- data/lib/ruby-bindgen/generators/rice/variable.erb +5 -0
- data/lib/ruby-bindgen/inputter.rb +54 -0
- data/lib/ruby-bindgen/name_mapper.rb +65 -0
- data/lib/ruby-bindgen/namer.rb +138 -0
- data/lib/ruby-bindgen/outputter.rb +40 -0
- data/lib/ruby-bindgen/parser.rb +82 -0
- data/lib/ruby-bindgen/refinements/cursor.rb +57 -0
- data/lib/ruby-bindgen/refinements/string.rb +41 -0
- data/lib/ruby-bindgen/symbol_candidates.rb +282 -0
- data/lib/ruby-bindgen/symbol_entry.rb +21 -0
- data/lib/ruby-bindgen/symbols.rb +107 -0
- data/lib/ruby-bindgen/type_pointer_formatter.rb +35 -0
- data/lib/ruby-bindgen/version.rb +3 -0
- data/lib/ruby-bindgen.rb +19 -0
- data/ruby-bindgen.gemspec +52 -0
- metadata +260 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Type Spelling in `ruby-bindgen`
|
|
2
|
+
|
|
3
|
+
This document explains why generating correct, fully-qualified C++ type spellings from libclang is inherently complex, and how `ruby-bindgen` addresses that complexity.
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
- There is no single libclang API that always returns the right C++ spelling for code generation.
|
|
8
|
+
- Canonical types are often semantically correct but syntactically wrong for generated bindings.
|
|
9
|
+
- `ruby-bindgen` reconstructs type spellings based on cursor kind and context, with canonicalization only as a fallback.
|
|
10
|
+
|
|
11
|
+
The objective is **not** to compute a canonical or normalized type. The objective is to emit a C++ spelling that:
|
|
12
|
+
|
|
13
|
+
1. Compiles correctly at the binding site
|
|
14
|
+
2. Preserves user-facing API intent (typedefs, aliases, dependent names)
|
|
15
|
+
3. Is stable across compilers and standard library implementations
|
|
16
|
+
|
|
17
|
+
## The Problem
|
|
18
|
+
|
|
19
|
+
When generating Rice bindings, `ruby-bindgen` must emit fully-qualified C++ type names that are valid in generated code.
|
|
20
|
+
|
|
21
|
+
For example:
|
|
22
|
+
|
|
23
|
+
```cpp
|
|
24
|
+
// Required
|
|
25
|
+
Constructor<cv::Mat, const cv::Range&, const cv::Range&>
|
|
26
|
+
|
|
27
|
+
// Incorrect (missing namespace qualification)
|
|
28
|
+
Constructor<Mat, const Range&, const Range&>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
libclang exposes multiple APIs for retrieving type information, but **none provide a complete, correct spelling for all C++ constructs**.
|
|
32
|
+
|
|
33
|
+
This is a fundamental consequence of C++’s type system and libclang’s design goals.
|
|
34
|
+
|
|
35
|
+
## Why a Canonical-Based Approach Fails
|
|
36
|
+
|
|
37
|
+
An initial strategy was to rely on `type.canonical.spelling` and then filter out implementation details. This approach fails in several unavoidable cases.
|
|
38
|
+
|
|
39
|
+
### Failure Modes
|
|
40
|
+
|
|
41
|
+
| Feature | Result from `canonical.spelling` | Why this is incorrect |
|
|
42
|
+
|-------|----------------------------------|------------------------|
|
|
43
|
+
| **Typedefs / aliases** | `SizeArray` → `int[3]` | Destroys alias intent and public API spelling |
|
|
44
|
+
| **Templates** | `iterator` → `std::iterator` | Loses specialization and template context |
|
|
45
|
+
| **Dependent types** | Missing `typename`, incorrect qualification | Canonical types erase dependency information |
|
|
46
|
+
| **Namespaces** | Over- or under-qualified names | Ignores lexical context |
|
|
47
|
+
|
|
48
|
+
In addition, canonical spellings frequently expose **implementation details** that must never appear in generated bindings:
|
|
49
|
+
|
|
50
|
+
- `__gnu_cxx::__normal_iterator<...>` (libstdc++)
|
|
51
|
+
- `_Ty`, `_Alloc`, `_Vector_iterator` (MSVC STL)
|
|
52
|
+
|
|
53
|
+
Filtering these reliably is not possible without reconstructing the original spelling logic, which defeats the purpose of canonicalization.
|
|
54
|
+
|
|
55
|
+
### Key Insight
|
|
56
|
+
|
|
57
|
+
`canonical.spelling` answers:
|
|
58
|
+
|
|
59
|
+
> “What is this type semantically?”
|
|
60
|
+
|
|
61
|
+
`ruby-bindgen` must answer:
|
|
62
|
+
|
|
63
|
+
> “How must this type be written so that user code compiles correctly and reflects the original API?”
|
|
64
|
+
|
|
65
|
+
These questions are fundamentally different.
|
|
66
|
+
|
|
67
|
+
## What libclang Provides
|
|
68
|
+
|
|
69
|
+
libclang exposes several partial representations of a type, each optimized for a different purpose:
|
|
70
|
+
|
|
71
|
+
| API | Returns | Limitation |
|
|
72
|
+
|----|---------|------------|
|
|
73
|
+
| `type.spelling` | Source-level spelling | Often unqualified |
|
|
74
|
+
| `type.canonical.spelling` | Fully desugared type | Erases typedefs and context |
|
|
75
|
+
| `declaration.qualified_name` | Namespace-qualified name | Drops template arguments |
|
|
76
|
+
| `declaration.qualified_display_name` | Name + template parameters | May omit enclosing namespaces |
|
|
77
|
+
|
|
78
|
+
No single API preserves both **spelling fidelity** and **correct qualification**.
|
|
79
|
+
|
|
80
|
+
## `ruby-bindgen`’s Strategy
|
|
81
|
+
|
|
82
|
+
`ruby-bindgen` does not attempt to normalize all types through a single representation.
|
|
83
|
+
|
|
84
|
+
Instead, `type_spelling` reconstructs the correct spelling **based on cursor kind and context**, using canonical information only as a constrained fallback.
|
|
85
|
+
|
|
86
|
+
### Design Principle
|
|
87
|
+
|
|
88
|
+
> **Spelling fidelity is primary; canonicalization is secondary and opportunistic.**
|
|
89
|
+
|
|
90
|
+
### Cursor-Specific Handling
|
|
91
|
+
|
|
92
|
+
#### `cursor_class_template`
|
|
93
|
+
|
|
94
|
+
Template definitions (e.g., `template<typename T> class Vec`).
|
|
95
|
+
|
|
96
|
+
- Reconstruct template arguments explicitly
|
|
97
|
+
- Use `qualify_dependent_types_in_template_args`
|
|
98
|
+
- Do not consult `@type_name_map` (template parameters must remain dependent)
|
|
99
|
+
|
|
100
|
+
#### `cursor_typedef_decl` inside a class template
|
|
101
|
+
|
|
102
|
+
Dependent typedefs (e.g., `DataType<_Tp>::value_type`).
|
|
103
|
+
|
|
104
|
+
- Emit `typename` (required by the C++ standard)
|
|
105
|
+
- Combine `qualified_name` with `qualified_display_name`
|
|
106
|
+
- Preserve dependency instead of resolving it
|
|
107
|
+
|
|
108
|
+
#### `cursor_typedef_decl` (non-dependent)
|
|
109
|
+
|
|
110
|
+
Public typedefs (e.g., `typedef Point_<int> Point2i`).
|
|
111
|
+
|
|
112
|
+
- Preserve the typedef name
|
|
113
|
+
- Do not desugar to the underlying type
|
|
114
|
+
- Qualify template arguments via `@type_name_map`
|
|
115
|
+
|
|
116
|
+
#### `cursor_type_alias_decl`
|
|
117
|
+
|
|
118
|
+
C++11 `using` declarations.
|
|
119
|
+
|
|
120
|
+
- Treated identically to `cursor_typedef_decl`
|
|
121
|
+
- Required for cross-compiler support (MSVC favors `using`)
|
|
122
|
+
|
|
123
|
+
#### `cursor_class_decl` and related types
|
|
124
|
+
|
|
125
|
+
Concrete types and template instantiations.
|
|
126
|
+
|
|
127
|
+
- Start from `fully_qualified_name`
|
|
128
|
+
- Optionally consult `canonical.spelling`
|
|
129
|
+
- Only when it does not introduce implementation types
|
|
130
|
+
- Qualify template arguments using `qualify_template_args`
|
|
131
|
+
|
|
132
|
+
## The `@type_name_map`
|
|
133
|
+
|
|
134
|
+
During translation unit processing, `ruby-bindgen` builds a map from simple identifiers to fully-qualified names:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
{
|
|
138
|
+
"Range" => "cv::Range",
|
|
139
|
+
"Mat" => "cv::Mat",
|
|
140
|
+
"Pixel" => "iter::Pixel"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This map is used to qualify **unqualified template arguments**, not to rewrite dependent names or template parameters.
|
|
145
|
+
|
|
146
|
+
## Where Canonicalization Works
|
|
147
|
+
|
|
148
|
+
`canonical.spelling` is useful only in limited cases:
|
|
149
|
+
|
|
150
|
+
- Non-dependent `cursor_class_decl` types
|
|
151
|
+
- Situations where alias preservation is irrelevant
|
|
152
|
+
- As a fallback sanity check for namespace qualification
|
|
153
|
+
|
|
154
|
+
It is not the primary source of truth.
|
|
155
|
+
|
|
156
|
+
## Why Not Use Clang’s C++ API?
|
|
157
|
+
|
|
158
|
+
Clang’s C++ API (`libTooling`) provides:
|
|
159
|
+
|
|
160
|
+
- `PrintingPolicy`
|
|
161
|
+
- Direct AST printers for fully-qualified names
|
|
162
|
+
- Precise control over dependent type emission
|
|
163
|
+
|
|
164
|
+
However:
|
|
165
|
+
|
|
166
|
+
- `ruby-bindgen` uses `ffi-clang`, which exposes only libclang’s C API
|
|
167
|
+
- libclang is designed for IDEs and static analysis tools, not code generation
|
|
168
|
+
- Reimplementing spelling logic is unavoidable in this environment
|
|
169
|
+
|
|
170
|
+
`ruby-bindgen`’s type spelling logic exists specifically to bridge this gap.
|
|
171
|
+
|
|
172
|
+
## Summary
|
|
173
|
+
|
|
174
|
+
There is no single libclang call that can produce correct C++ type spellings in all cases.
|
|
175
|
+
|
|
176
|
+
The complexity in `ruby-bindgen` is inherent:
|
|
177
|
+
|
|
178
|
+
- C++ has typedefs, aliases, templates, dependent types, and contextual name lookup
|
|
179
|
+
- libclang exposes these differently depending on cursor kind
|
|
180
|
+
- Correct code generation requires spelling reconstruction, not canonicalization
|
|
181
|
+
|
|
182
|
+
The current design reflects these constraints deliberately.
|
|
183
|
+
|
|
184
|
+
## Code Locations
|
|
185
|
+
|
|
186
|
+
All type-spelling logic lives in the `RubyBindgen::Generators::Rice::TypeSpeller` class:
|
|
187
|
+
|
|
188
|
+
- `lib/ruby-bindgen/generators/rice/type_speller.rb`
|
|
189
|
+
- Top-level entry points: `type_spelling`, `type_spellings`,
|
|
190
|
+
`qualified_class_name`, `qualified_display_name`
|
|
191
|
+
- Declared / unexposed type handling: `type_spelling_declared`,
|
|
192
|
+
`type_spelling_unexposed`, `type_spelling_pointer`
|
|
193
|
+
- Template handling: `qualify_template_args`,
|
|
194
|
+
`qualify_dependent_types_in_template_args`,
|
|
195
|
+
`qualify_template_parameter_packs`,
|
|
196
|
+
`qualify_class_template_typedefs`
|
|
197
|
+
- Static member qualification: `qualify_class_static_members`
|
|
198
|
+
|
|
199
|
+
The `fully_qualified_name` helper used by `TypeSpeller` is provided
|
|
200
|
+
directly by `FFI::Clang::Type` (ffi-clang ≥ 0.16).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Updating Bindings
|
|
2
|
+
|
|
3
|
+
After generating bindings with `ruby-bindgen`, you'll need to regenerate them periodically as the upstream library evolves, `ruby-bindgen` improves, or you upgrade dependencies. This document covers strategies for managing that process.
|
|
4
|
+
|
|
5
|
+
## Simple Case
|
|
6
|
+
|
|
7
|
+
If you have no [manual edits or refinements](cpp/customizing.md) to preserve, you can simply regenerate the bindings. Configuration changes such as adding new skip patterns or version guards are fine — they are read from the YAML file each time.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
ruby-bindgen bindings.yaml
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
In most cases, the updated bindings should compile and work.
|
|
14
|
+
|
|
15
|
+
The rest of this page discusses strategies for more complex cases where you want to preserve [customizations](cpp/customizing.md) when regenerating bindings.
|
|
16
|
+
|
|
17
|
+
## Preserving Refinements
|
|
18
|
+
|
|
19
|
+
[Refinements](cpp/customizing.md#refinements) live in a separate directory that `ruby-bindgen` never touches, so the source files survive regeneration automatically. However, the top-level `-rb.cpp` file is regenerated, so you will need to re-add the `#include` directives and `Init_*_Refinements` calls.
|
|
20
|
+
|
|
21
|
+
## Preserving Manual Edits
|
|
22
|
+
|
|
23
|
+
If you have [manual edits](cpp/customizing.md#manual-edits) to generated files, you need a way to reapply them after regeneration. Two possible approaches include:
|
|
24
|
+
|
|
25
|
+
### Option A: Diff File
|
|
26
|
+
|
|
27
|
+
Maintain a diff file that can be reapplied after regeneration:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# After making all manual changes to generated files
|
|
31
|
+
git diff -- ext/ ':(exclude)ext/manual_updates.md' > ext/manual_updates.diff
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
After regenerating:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Regenerate
|
|
38
|
+
ruby-bindgen bindings.yaml
|
|
39
|
+
|
|
40
|
+
# Reapply manual changes
|
|
41
|
+
cd ext/
|
|
42
|
+
git apply manual_updates.diff
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Pros**: Simple, standard tooling, machine-readable.
|
|
46
|
+
|
|
47
|
+
**Cons**: Diffs are fragile. If `ruby-bindgen`'s output changes line numbers or formatting, the diff won't apply cleanly. You'll need to manually resolve failures and regenerate the diff.
|
|
48
|
+
|
|
49
|
+
### Option B: Agent File
|
|
50
|
+
|
|
51
|
+
Maintain a structured document (`manual_updates.md`) that describes each change declaratively. An AI assistant or a human can follow the instructions after each regeneration. For an example, see opencv-ruby's [manual_updates.md](https://github.com/cfis/opencv-ruby/blob/main/ext/manual_updates.md).
|
|
52
|
+
|
|
53
|
+
**Pros**: Survives large formatting changes, human-readable, an AI assistant can apply the instructions even when line numbers shift.
|
|
54
|
+
|
|
55
|
+
**Cons**: Must be kept in sync with actual changes.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Version Guards
|
|
2
|
+
|
|
3
|
+
Some libraries ship symbols that only exist in certain versions. For example, OpenCV 4.5 added CUDA codec APIs that don't exist in 4.1. Version guards let you generate bindings that work across multiple library versions by wrapping version-specific symbols in conditional checks.
|
|
4
|
+
|
|
5
|
+
The mechanism differs by format:
|
|
6
|
+
|
|
7
|
+
- **Rice (C++)** — `#if` / `#endif` preprocessor directives checked at compile time
|
|
8
|
+
- **FFI (C)** — Ruby `if` conditionals checked at runtime via a user-implemented version method
|
|
9
|
+
|
|
10
|
+
## Rice (C++)
|
|
11
|
+
|
|
12
|
+
### Configuration
|
|
13
|
+
|
|
14
|
+
Two config options work together:
|
|
15
|
+
|
|
16
|
+
1. **`version_check`** — the C preprocessor macro to test (e.g., `CV_VERSION`)
|
|
17
|
+
2. **`symbols.versions`** — which symbols to guard and at what version
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
format: Rice
|
|
21
|
+
version_check: CV_VERSION
|
|
22
|
+
symbols:
|
|
23
|
+
versions:
|
|
24
|
+
40100:
|
|
25
|
+
- cv::Foo::newMethod
|
|
26
|
+
40500:
|
|
27
|
+
- /cv::cuda::.*/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Symbol names support the same syntax as skip symbols: simple names, fully qualified names, signatures, and regex patterns. See [Symbols](configuration.md#symbols) for details.
|
|
31
|
+
|
|
32
|
+
### Generated Output
|
|
33
|
+
|
|
34
|
+
#### Class methods
|
|
35
|
+
|
|
36
|
+
Version-guarded methods within a class produce inline `#if` / `#endif` around the chained method definition:
|
|
37
|
+
|
|
38
|
+
```cpp
|
|
39
|
+
Rice::Data_Type<cv::Foo> rb_cFoo = define_class<cv::Foo>("Foo")
|
|
40
|
+
.define_method("bar", &cv::Foo::bar)
|
|
41
|
+
#if CV_VERSION >= 40100
|
|
42
|
+
.define_method("new_method", &cv::Foo::newMethod)
|
|
43
|
+
#endif
|
|
44
|
+
;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Top-level functions
|
|
48
|
+
|
|
49
|
+
Version-guarded free functions are wrapped at the statement level:
|
|
50
|
+
|
|
51
|
+
```cpp
|
|
52
|
+
#if CV_VERSION >= 40100
|
|
53
|
+
define_global_function("new_func", &cv::newFunc);
|
|
54
|
+
#endif
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### How It Works
|
|
58
|
+
|
|
59
|
+
When `version_check` is set, the generator looks up each symbol's version via the `symbols` config. Symbols with a version are grouped by their version value. The code generator emits `#if VERSION_MACRO >= version` before the group and `#endif` after it. Unversioned symbols are emitted normally with no guards.
|
|
60
|
+
|
|
61
|
+
## FFI (C)
|
|
62
|
+
|
|
63
|
+
For C libraries, version detection happens at runtime. When `symbols.versions` has entries, the generator:
|
|
64
|
+
|
|
65
|
+
1. Wraps version-specific symbols in `if {version_check} >= version` conditionals
|
|
66
|
+
2. Adds `require_relative '{project}_version'` to the project loader file
|
|
67
|
+
3. Generates a `{project}_version.rb` skeleton (once — won't overwrite if it already exists)
|
|
68
|
+
|
|
69
|
+
The user implements the `version_check` method by calling the library's own version API. See [Version Detection](c/version_guards.md) for a complete example using PROJ.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
|
|
4
|
+
module RubyBindgen
|
|
5
|
+
class Config
|
|
6
|
+
def initialize(config_path)
|
|
7
|
+
@config_dir = File.dirname(File.expand_path(config_path))
|
|
8
|
+
raw = YAML.safe_load(File.read(config_path), permitted_classes: [], permitted_symbols: [], aliases: true)
|
|
9
|
+
# YAML.safe_load returns nil for an empty file. Treat that as an empty
|
|
10
|
+
# config so the CLI's validate_config can produce its useful "Config
|
|
11
|
+
# must specify 'output'" message instead of a NoMethodError.
|
|
12
|
+
@data = raw.is_a?(Hash) ? symbolize_keys(raw) : {}
|
|
13
|
+
resolve_toolchain
|
|
14
|
+
resolve_paths
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def [](key)
|
|
18
|
+
@data[key]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api private
|
|
22
|
+
# Used by tests for ad-hoc overrides and by the CLI to default :input to
|
|
23
|
+
# :output. Not intended for downstream code; the public contract is read-only.
|
|
24
|
+
def []=(key, value)
|
|
25
|
+
@data[key] = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def resolve_toolchain
|
|
31
|
+
toolchain = RUBY_PLATFORM =~ /mswin/ ? :'clang-cl' : :clang
|
|
32
|
+
|
|
33
|
+
toolchain_config = @data[toolchain]
|
|
34
|
+
if toolchain_config.is_a?(Hash)
|
|
35
|
+
@data[:libclang] = toolchain_config[:libclang]
|
|
36
|
+
@data[:clang_args] = toolchain_config[:args]&.map do |arg|
|
|
37
|
+
# Resolve relative -I paths relative to config directory
|
|
38
|
+
arg.start_with?('-I') ? "-I#{File.expand_path(arg[2..], @config_dir)}" : arg
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve_paths
|
|
44
|
+
@data[:input] = resolve_path(@data[:input]) if @data[:input]
|
|
45
|
+
@data[:output] = resolve_path(@data[:output]) if @data[:output]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve_path(path)
|
|
49
|
+
return path if Pathname.new(path).absolute?
|
|
50
|
+
File.expand_path(path, @config_dir)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def symbolize_keys(hash)
|
|
54
|
+
hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }.transform_values do |value|
|
|
55
|
+
case value
|
|
56
|
+
when Hash then symbolize_keys(value)
|
|
57
|
+
when Array then value.map { |v| v.is_a?(Hash) ? symbolize_keys(v) : v }
|
|
58
|
+
else value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
module RubyBindgen
|
|
2
|
+
module Generators
|
|
3
|
+
class CMake < Generator
|
|
4
|
+
GuardedEntry = Data.define(:condition, :directories, :files)
|
|
5
|
+
|
|
6
|
+
def self.template_dir
|
|
7
|
+
__dir__
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def include_dirs
|
|
11
|
+
config[:include_dirs] || []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def guards
|
|
15
|
+
@guards ||= begin
|
|
16
|
+
config_guards = config[:guards] || {}
|
|
17
|
+
config_guards.map do |condition, patterns|
|
|
18
|
+
Guard.new(condition: condition, patterns: patterns, base_path: @inputter.base_path)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def generate
|
|
24
|
+
base = Pathname.new(@inputter.base_path)
|
|
25
|
+
|
|
26
|
+
# Collect files from inputter, grouped by relative directory
|
|
27
|
+
files_by_dir = Hash.new do |hash, key|
|
|
28
|
+
hash[key] = []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@inputter.each do |path, relative_path|
|
|
32
|
+
dir = File.dirname(relative_path)
|
|
33
|
+
files_by_dir[dir] << Pathname.new(path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Collect all directories that need CMakeLists.txt files
|
|
37
|
+
# (directories containing files, plus all ancestor directories)
|
|
38
|
+
all_dirs = Set.new
|
|
39
|
+
files_by_dir.each_key do |dir|
|
|
40
|
+
next if dir == "."
|
|
41
|
+
parts = Pathname.new(dir).each_filename.to_a
|
|
42
|
+
parts.length.times do |i|
|
|
43
|
+
all_dirs << parts[0..i].join("/")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Build parent -> immediate child directories mapping
|
|
48
|
+
child_dirs_of = Hash.new do |hash, key|
|
|
49
|
+
hash[key] = []
|
|
50
|
+
end
|
|
51
|
+
all_dirs.each do |dir|
|
|
52
|
+
parent = File.dirname(dir)
|
|
53
|
+
parent = "." if parent == dir
|
|
54
|
+
child_dirs_of[parent] << base.join(dir)
|
|
55
|
+
end
|
|
56
|
+
child_dirs_of.each_value(&:sort!)
|
|
57
|
+
|
|
58
|
+
file_guards, directory_guards = build_guard_maps(files_by_dir, all_dirs.to_a)
|
|
59
|
+
|
|
60
|
+
if @project
|
|
61
|
+
# Root CMakeLists.txt
|
|
62
|
+
content = render_template("project",
|
|
63
|
+
:project => self.project,
|
|
64
|
+
:directories => unguarded_paths(child_dirs_of["."], directory_guards, base),
|
|
65
|
+
:files => unguarded_paths(files_by_dir["."].sort, file_guards, base),
|
|
66
|
+
:guarded_entries => guarded_entries(child_dirs_of["."],
|
|
67
|
+
directory_guards,
|
|
68
|
+
files_by_dir["."].sort,
|
|
69
|
+
file_guards,
|
|
70
|
+
base),
|
|
71
|
+
:include_dirs => self.include_dirs)
|
|
72
|
+
self.outputter.write("CMakeLists.txt", content)
|
|
73
|
+
|
|
74
|
+
# Presets
|
|
75
|
+
content = render_template("presets")
|
|
76
|
+
self.outputter.write("CMakePresets.json", content)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Subdirectory CMakeLists.txt files
|
|
80
|
+
all_dirs.sort.each do |dir|
|
|
81
|
+
content = render_template("directory",
|
|
82
|
+
:project => self.project,
|
|
83
|
+
:directories => unguarded_paths(child_dirs_of[dir], directory_guards, base),
|
|
84
|
+
:files => unguarded_paths((files_by_dir[dir] || []).sort, file_guards, base),
|
|
85
|
+
:guarded_entries => guarded_entries(child_dirs_of[dir],
|
|
86
|
+
directory_guards,
|
|
87
|
+
(files_by_dir[dir] || []).sort,
|
|
88
|
+
file_guards,
|
|
89
|
+
base))
|
|
90
|
+
self.outputter.write(File.join(dir, "CMakeLists.txt"), content)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def build_guard_maps(files_by_dir, all_dirs)
|
|
97
|
+
file_paths = files_by_dir.values.flatten.map do |path|
|
|
98
|
+
expand_path(path)
|
|
99
|
+
end.sort
|
|
100
|
+
directory_paths = all_dirs.map do |dir|
|
|
101
|
+
expand_path(dir, @inputter.base_path)
|
|
102
|
+
end.sort
|
|
103
|
+
file_guards = {}
|
|
104
|
+
directory_guards = {}
|
|
105
|
+
|
|
106
|
+
guards.each do |guard|
|
|
107
|
+
match = guard.match(file_paths: file_paths, directory_paths: directory_paths)
|
|
108
|
+
|
|
109
|
+
match.files.each do |path|
|
|
110
|
+
assign_guard!(file_guards, path, match.condition)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
match.directories.each do |path|
|
|
114
|
+
assign_guard!(directory_guards, path, match.condition)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
[file_guards, directory_guards]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def assign_guard!(assignments, path, condition)
|
|
122
|
+
previous = assignments[path]
|
|
123
|
+
if previous && previous != condition
|
|
124
|
+
raise ArgumentError, "#{path} matched multiple guard conditions: #{previous.inspect}, #{condition.inspect}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
assignments[path] = condition
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def guarded_entries(directories, directory_guards, files, file_guards, base)
|
|
131
|
+
grouped = Hash.new do |hash, key|
|
|
132
|
+
hash[key] = GuardedEntry.new(condition: key, directories: [], files: [])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
directories.each do |directory|
|
|
136
|
+
condition = directory_guards[expand_path(directory, base)]
|
|
137
|
+
next unless condition
|
|
138
|
+
|
|
139
|
+
grouped[condition].directories << directory
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
files.each do |file|
|
|
143
|
+
condition = file_guards[expand_path(file, base)]
|
|
144
|
+
next unless condition
|
|
145
|
+
|
|
146
|
+
grouped[condition].files << file
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
guards.map(&:condition).filter_map do |condition|
|
|
150
|
+
entry = grouped[condition]
|
|
151
|
+
next if entry.nil? || (entry.directories.empty? && entry.files.empty?)
|
|
152
|
+
|
|
153
|
+
entry
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def expand_path(path, base = @inputter.base_path)
|
|
158
|
+
File.expand_path(path.to_s, base.to_s)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def unguarded_paths(paths, guard_map, base)
|
|
162
|
+
paths.reject do |path|
|
|
163
|
+
guard_map.key?(expand_path(path, base))
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
require_relative 'guard'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated by ruby-bindgen (<%= RubyBindgen::VERSION %>)
|
|
2
|
+
|
|
3
|
+
# Subdirectories
|
|
4
|
+
<% directories.each do |directory| -%>
|
|
5
|
+
add_subdirectory ("<%= directory.relative_path_from(directory.parent) %>")
|
|
6
|
+
<% end -%>
|
|
7
|
+
|
|
8
|
+
# Sources
|
|
9
|
+
<% if !files.empty? -%>
|
|
10
|
+
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
|
|
11
|
+
<% files.each do |file| -%>
|
|
12
|
+
"<%= file.relative_path_from(file.parent) %>"
|
|
13
|
+
<% end -%>
|
|
14
|
+
)
|
|
15
|
+
<% end -%>
|
|
16
|
+
<% guarded_entries.each do |entry| -%>
|
|
17
|
+
if(<%= entry.condition %>)
|
|
18
|
+
<% entry.directories.each do |directory| -%>
|
|
19
|
+
add_subdirectory ("<%= directory.relative_path_from(directory.parent) %>")
|
|
20
|
+
<% end -%>
|
|
21
|
+
<% if !entry.files.empty? -%>
|
|
22
|
+
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
|
|
23
|
+
<% entry.files.each do |file| -%>
|
|
24
|
+
"<%= file.relative_path_from(file.parent) %>"
|
|
25
|
+
<% end -%>
|
|
26
|
+
)
|
|
27
|
+
<% end -%>
|
|
28
|
+
endif()
|
|
29
|
+
<% end -%>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module RubyBindgen
|
|
2
|
+
module Generators
|
|
3
|
+
class CMake
|
|
4
|
+
class Guard
|
|
5
|
+
GuardMatch = Data.define(:condition, :directories, :files)
|
|
6
|
+
|
|
7
|
+
attr_reader :condition, :patterns
|
|
8
|
+
|
|
9
|
+
def initialize(condition:, patterns:, base_path:)
|
|
10
|
+
@condition = condition.to_s
|
|
11
|
+
@patterns = Array(patterns).map do |pattern|
|
|
12
|
+
expand_path(pattern.to_s, base_path)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match(file_paths:, directory_paths:)
|
|
17
|
+
matched_files = []
|
|
18
|
+
matched_directories = []
|
|
19
|
+
|
|
20
|
+
patterns.each do |pattern|
|
|
21
|
+
files = file_paths.select do |path|
|
|
22
|
+
path_match?(pattern, path)
|
|
23
|
+
end
|
|
24
|
+
directories = directory_paths.select do |path|
|
|
25
|
+
path_match?(pattern, path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
warn_unmatched(pattern) if files.empty? && directories.empty?
|
|
29
|
+
|
|
30
|
+
matched_files.concat(files)
|
|
31
|
+
matched_directories.concat(directories)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
GuardMatch.new(condition: condition,
|
|
35
|
+
directories: matched_directories.uniq.sort,
|
|
36
|
+
files: matched_files.uniq.sort)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def path_match?(pattern, path)
|
|
42
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def expand_path(path, base)
|
|
46
|
+
File.expand_path(path.to_s, base.to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def warn_unmatched(pattern)
|
|
50
|
+
warn "CMake guard #{condition.inspect} did not match any generated paths for pattern #{pattern.inspect}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|