1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
use boltffi_ffi_rules::naming::{LibraryName, Name};
use super::super::ast::{CSharpClassName, CSharpNamespace};
use super::{
CFunctionName, CSharpCallablePlan, CSharpCallbackPlan, CSharpClassPlan, CSharpClosurePlan,
CSharpEnumPlan, CSharpFunctionPlan, CSharpRecordPlan,
};
/// A whole C# module: namespace, library binding, and every record, enum,
/// and function it exposes. Renders into a `namespace` spread across
/// multiple `.cs` files: one per record (`{record_name}.cs`), one per enum
/// (`{enum_name}.cs`), and a shared file (`{class_name}.cs`) holding the
/// function wrappers, the `NativeMethods` DllImport class, and the
/// runtime helpers (`FfiBuf`, `WireReader`, `WireWriter`) gated by the
/// `needs_*` predicates.
#[derive(Debug, Clone)]
pub struct CSharpModulePlan {
/// Namespace for the generated files.
pub namespace: CSharpNamespace,
/// Top-level wrapper class name.
pub class_name: CSharpClassName,
/// Native library name used in `[DllImport("...")]` declarations.
pub lib_name: Name<LibraryName>,
/// C function that frees the buffer used by wire-encoded returns.
pub free_buf_ffi_name: CFunctionName,
/// Records exposed by the module. Each record is rendered to its own
/// `.cs` file as a `readonly record struct`.
pub records: Vec<CSharpRecordPlan>,
/// Enums exposed by the module. Each enum is rendered to its own `.cs`
/// file: C-style as a native `enum`, data-carrying as an
/// `abstract record` with nested `sealed record` variants.
pub enums: Vec<CSharpEnumPlan>,
/// Top-level functions exposed by the module.
pub functions: Vec<CSharpFunctionPlan>,
/// Classes exposed by the module. Each class is rendered to its
/// own `.cs` file as a `sealed class` implementing `IDisposable`
/// around an opaque native handle.
pub classes: Vec<CSharpClassPlan>,
/// Callback trait interfaces and bridges.
pub callbacks: Vec<CSharpCallbackPlan>,
/// Closure delegate types and bridges.
pub closures: Vec<CSharpClosurePlan>,
}
impl CSharpModulePlan {
/// Whether the module exposes any functions. Gates the wrapper-class
/// file in the functions template.
pub fn has_functions(&self) -> bool {
!self.functions.is_empty()
}
/// Whether the module exposes any classes. Gates the per-class
/// `[DllImport]` block in the native template.
pub fn has_classes(&self) -> bool {
!self.classes.is_empty()
}
pub fn has_streams(&self) -> bool {
self.classes.iter().any(CSharpClassPlan::has_streams)
}
pub fn has_async(&self) -> bool {
self.functions.iter().any(CSharpFunctionPlan::is_async)
|| self.classes.iter().any(CSharpClassPlan::has_async_methods)
|| self.records.iter().any(CSharpRecordPlan::has_async_methods)
|| self.enums.iter().any(CSharpEnumPlan::has_async_methods)
}
pub fn has_callbacks(&self) -> bool {
!self.callbacks.is_empty()
}
pub fn has_closures(&self) -> bool {
!self.closures.is_empty()
}
pub fn has_async_callbacks(&self) -> bool {
self.callbacks
.iter()
.any(|callback| callback.has_async_methods)
}
pub fn needs_callback_runtime(&self) -> bool {
self.has_callbacks() || self.has_closures()
}
pub fn needs_ffi_status(&self) -> bool {
self.has_async() || self.needs_callback_runtime()
}
/// Whether the module needs `using System.Text;`. True when any function
/// or class member touches a string (param or wire-decoded return), or
/// any record has a string field, since `Encoding.UTF8.GetBytes` lives
/// there. Decoding does not need `System.Text`; `WireReader` reads
/// strings via `Marshal.PtrToStringUTF8`.
pub fn needs_system_text(&self) -> bool {
if self.needs_wire_writer() {
return true;
}
self.functions
.iter()
.any(|f| f.params.iter().any(|p| p.csharp_type.contains_string()))
|| self.classes.iter().any(CSharpClassPlan::needs_system_text)
|| self.records.iter().any(CSharpRecordPlan::has_string_fields)
}
/// Whether any function, class constructor, or class method takes a
/// wire-encoded param. Blittable record params pass through the CLR
/// as direct struct values and do not contribute here.
fn has_wire_params(&self) -> bool {
self.functions.iter().any(|f| !f.wire_writers.is_empty())
|| self.classes.iter().any(CSharpClassPlan::has_wire_params)
}
/// Whether any function returns through an `FfiBuf`, a wire-decoded
/// string or non-blittable record. Blittable records come back as
/// direct struct values and do not count here.
fn has_ffi_buf_returns(&self) -> bool {
self.functions
.iter()
.any(|f| f.return_kind.native_returns_ffi_buf())
|| self
.classes
.iter()
.flat_map(|c| c.methods.iter())
.any(|m| m.return_kind.native_returns_ffi_buf())
|| self
.records
.iter()
.flat_map(|r| r.methods.iter())
.any(|m| m.return_kind.native_returns_ffi_buf())
|| self
.enums
.iter()
.flat_map(|e| e.methods.iter())
.any(|m| m.return_kind.native_returns_ffi_buf())
}
/// Whether the `FfiBuf` struct and `FreeBuf` DllImport are emitted.
/// Needed for wire-encoded returns, and pulled in whenever a record or
/// enum exists so the `WireReader` (which takes `FfiBuf`) compiles.
pub fn needs_ffi_buf(&self) -> bool {
self.has_ffi_buf_returns()
|| !self.records.is_empty()
|| !self.enums.is_empty()
|| self.callbacks.iter().any(|callback| callback.needs_ffi_buf)
|| self.closures.iter().any(|closure| closure.needs_ffi_buf)
}
/// Whether the stateful `WireReader` helper is emitted. Needed for
/// wire-decoded returns, for any record's `Decode` method, and for the
/// enum wire helpers (`StatusWire.Decode`, `Shape.Decode`).
pub fn needs_wire_reader(&self) -> bool {
self.has_ffi_buf_returns()
|| !self.records.is_empty()
|| !self.enums.is_empty()
|| self
.callbacks
.iter()
.any(|callback| callback.needs_wire_reader)
|| self
.closures
.iter()
.any(|closure| closure.needs_wire_reader)
}
/// Whether the `WireWriter` helper is emitted. Needed for wire-encoded
/// params, for any record's `WireEncodeTo` method, and for the enum
/// encode helpers.
pub fn needs_wire_writer(&self) -> bool {
self.has_wire_params()
|| !self.records.is_empty()
|| !self.enums.is_empty()
|| self
.callbacks
.iter()
.any(|callback| callback.needs_wire_writer)
|| self
.closures
.iter()
.any(|closure| closure.needs_wire_writer)
}
/// Whether the runtime `BoltException` class is emitted. True when
/// any throwing function or method in the module ends up calling
/// `new BoltException(...)` — i.e., any `Result<_, _>` whose Err
/// type isn't a typed `#[error]` enum or record. Mirrors the
/// Kotlin/Swift/Dart pattern of a generated runtime FFI exception
/// type; Java reuses the built-in `RuntimeException` instead and
/// has no equivalent.
pub fn needs_bolt_exception(&self) -> bool {
self.functions.iter().any(|f| f.return_kind.is_result())
|| self
.classes
.iter()
.any(CSharpClassPlan::has_throwing_methods)
|| self
.records
.iter()
.any(CSharpRecordPlan::has_throwing_methods)
|| self.enums.iter().any(CSharpEnumPlan::has_throwing_methods)
}
}
#[cfg(test)]
mod tests {
use super::super::super::ast::{
CSharpExpression, CSharpIdentity, CSharpLocalName, CSharpMethodName, CSharpType,
};
use super::super::{
CFunctionName, CSharpEnumKind, CSharpFunctionPlan, CSharpMethodPlan, CSharpReceiver,
CSharpReturnKind,
};
use super::*;
fn dummy_throw_expr() -> CSharpExpression {
CSharpExpression::Identity(CSharpIdentity::Local(CSharpLocalName::new("placeholder")))
}
fn empty_module() -> CSharpModulePlan {
CSharpModulePlan {
namespace: CSharpNamespace::from_source("Demo"),
class_name: CSharpClassName::from_source("demo"),
lib_name: Name::new("demo".to_string()),
free_buf_ffi_name: CFunctionName::new("boltffi_free_buf".to_string()),
records: vec![],
enums: vec![],
functions: vec![],
classes: vec![],
callbacks: vec![],
closures: vec![],
}
}
fn throwing_function() -> CSharpFunctionPlan {
CSharpFunctionPlan {
summary_doc: None,
name: CSharpMethodName::from_source("test"),
params: vec![],
return_type: CSharpType::Int,
return_kind: CSharpReturnKind::WireDecodeResult {
ok_decode_expr: None,
err_throw_expr: dummy_throw_expr(),
},
ffi_name: CFunctionName::new("boltffi_test".to_string()),
async_call: None,
wire_writers: vec![],
}
}
fn throwing_method() -> CSharpMethodPlan {
CSharpMethodPlan {
summary_doc: None,
name: CSharpMethodName::from_source("test"),
native_method_name: CSharpMethodName::from_source("OwnerTest"),
ffi_name: CFunctionName::new("boltffi_test".to_string()),
async_call: None,
receiver: CSharpReceiver::ClassInstance,
params: vec![],
return_type: CSharpType::Void,
return_kind: CSharpReturnKind::WireDecodeResult {
ok_decode_expr: None,
err_throw_expr: dummy_throw_expr(),
},
wire_writers: vec![],
owner_is_blittable: false,
}
}
fn throwing_class_plan() -> CSharpClassPlan {
CSharpClassPlan {
summary_doc: None,
class_name: CSharpClassName::from_source("counter"),
ffi_free: CFunctionName::new("boltffi_counter_free".to_string()),
native_free_method_name: CSharpMethodName::from_source("CounterFree"),
constructors: vec![],
methods: vec![throwing_method()],
streams: vec![],
}
}
fn throwing_record_plan() -> CSharpRecordPlan {
CSharpRecordPlan {
summary_doc: None,
class_name: CSharpClassName::from_source("dataset"),
is_blittable: false,
fields: vec![],
methods: vec![throwing_method()],
is_error: false,
}
}
fn throwing_enum_plan() -> CSharpEnumPlan {
CSharpEnumPlan {
summary_doc: None,
class_name: CSharpClassName::from_source("status"),
wire_class_name: CSharpClassName::from_source("status_wire"),
methods_class_name: None,
kind: CSharpEnumKind::CStyle,
underlying_type: None,
variants: vec![],
methods: vec![throwing_method()],
is_error: false,
}
}
/// A module with no throwing functions or members doesn't need the
/// runtime `BoltException` class. Pinning the negative case prevents
/// the predicate from drifting into "always true" and unconditionally
/// emitting the class.
#[test]
fn needs_bolt_exception_is_false_for_empty_module() {
assert!(!empty_module().needs_bolt_exception());
}
/// A throwing top-level function flips the predicate. Function
/// wrappers can throw `BoltException` directly even when no class /
/// record / enum has a throwing method, so the function path has to
/// trigger the runtime class on its own.
#[test]
fn needs_bolt_exception_is_true_when_a_function_returns_result() {
let mut module = empty_module();
module.functions.push(throwing_function());
assert!(module.needs_bolt_exception());
}
/// A class with a throwing method flips the predicate. The
/// generated wrapper reaches `throw new BoltException(...)` from
/// inside the class even if no top-level function does.
#[test]
fn needs_bolt_exception_is_true_when_a_class_method_returns_result() {
let mut module = empty_module();
module.classes.push(throwing_class_plan());
assert!(module.needs_bolt_exception());
}
/// A record method that returns `Result<_, _>` flips the predicate
/// — record methods aren't on classes, so the class-only check
/// would miss them and the runtime class would silently not emit.
#[test]
fn needs_bolt_exception_is_true_when_a_record_method_returns_result() {
let mut module = empty_module();
module.records.push(throwing_record_plan());
assert!(module.needs_bolt_exception());
}
/// Same for enum methods. Pinning all four input sources separately
/// catches the case where someone refactors the predicate and
/// forgets one branch.
#[test]
fn needs_bolt_exception_is_true_when_an_enum_method_returns_result() {
let mut module = empty_module();
module.enums.push(throwing_enum_plan());
assert!(module.needs_bolt_exception());
}
}