apache-datasketches-sys 0.2.0

Raw cxx bridge to Apache DataSketches C++ (do not use directly; see apache-datasketches)
Documentation
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
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
/// Panics if `path` does not exist on disk.
///
/// The bridge and shim file lists in `main` below are expected to be
/// exhaustive for each completed sketch family: every file named there
/// should actually exist, and every file that exists should be named
/// there. A missing file at this point means either a typo in one of
/// those lists, or a file that was renamed/moved/deleted without
/// updating the list to match -- both are bugs we want to catch as a
/// clear build failure here, not as a confusing link error or missing
/// symbol surfacing later.
fn require_exists(path: &str) {
    if !std::path::Path::new(path).exists() {
        panic!(
            "apache-datasketches-sys build.rs: expected file `{path}` does not exist.\n\n\
             This file is listed in build.rs as part of a completed sketch family's bridge/shim \
             file set, which is expected to be exhaustive. A missing file here means either a typo \
             in the file list in build.rs, or the file was renamed, moved, or deleted without \
             updating that list. Fix the list or restore the file."
        );
    }
}

fn main() {
    let mut bridges: Vec<&str> = Vec::new();

    if cfg!(feature = "hll") {
        bridges.push("src/hll.rs");
    }
    if cfg!(feature = "theta") {
        for path in [
            "src/theta_sketch.rs",
            "src/theta_compact.rs",
            "src/theta_wrapped.rs",
            "src/theta_union.rs",
            "src/theta_intersection.rs",
            "src/theta_a_not_b.rs",
            "src/theta_jaccard.rs",
        ] {
            require_exists(path);
            bridges.push(path);
        }
    }
    if cfg!(feature = "cpc") {
        for path in ["src/cpc_sketch.rs", "src/cpc_union.rs"] {
            require_exists(path);
            bridges.push(path);
        }
    }
    if cfg!(feature = "tuple") {
        // Note: src/array_of_doubles_input.rs and src/tuple_generic_input.rs
        // are deliberately absent — they are plain Rust modules, not cxx
        // bridges.
        for path in [
            "src/array_of_doubles_sketch.rs",
            "src/array_of_doubles_compact.rs",
            "src/array_of_doubles_union.rs",
            "src/array_of_doubles_intersection.rs",
            "src/array_of_doubles_a_not_b.rs",
            "src/array_of_doubles_jaccard.rs",
            "src/tuple_generic.rs",
            "src/tuple_generic_union.rs",
            "src/tuple_generic_intersection.rs",
            "src/tuple_generic_a_not_b.rs",
            "src/tuple_generic_jaccard.rs",
        ] {
            require_exists(path);
            bridges.push(path);
        }
    }

    check_bridge_name_uniqueness(&bridges);

    if bridges.is_empty() {
        return;
    }

    // We build against a copy of the needed datasketches-cpp headers
    // vendored into this crate (`vendor/datasketches-cpp`), not the
    // workspace-root git submodule: crates.io packaging only includes files
    // inside the crate directory, so a path escaping it via `../` would be
    // missing from the published tarball. The workspace-root submodule
    // remains the source of truth for updating the pinned version (see
    // vendor/README.md); this copy is refreshed from it manually.
    let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
    let vendor_dir = manifest_dir.join("vendor/datasketches-cpp");

    // cxx-build generates each bridge's header at
    // OUT_DIR/cxxbridge/include/<pkg-name>/src/<name>.rs.h, but our shim
    // headers include it as a bare "<name>.rs.h", so we need that directory
    // directly on the include path in addition to cxx_build::bridges'
    // default dirs.
    let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
    let generated_header_dir = out_dir
        .join("cxxbridge/include")
        .join(env!("CARGO_PKG_NAME"))
        .join("src");

    let mut build = cxx_build::bridges(&bridges);
    build
        .include(vendor_dir.join("common/include"))
        .include(vendor_dir.join("hll/include"))
        .include(vendor_dir.join("theta/include"))
        .include(vendor_dir.join("cpc/include"))
        .include(vendor_dir.join("tuple/include"))
        .include("cpp")
        .include("cpp/hll")
        .include("cpp/theta")
        .include("cpp/cpc")
        .include("cpp/tuple")
        .include(generated_header_dir)
        .flag_if_supported("-std=c++17")
        // Upstream datasketches-cpp declares virtual destructors on a couple
        // of `final` classes (e.g. hll_sketch_alloc, AuxHashMap) — harmless,
        // but noisy under clang. Silenced here rather than patched in the
        // vendored headers so we don't diverge from upstream.
        .flag_if_supported("-Wno-unnecessary-virtual-specifier");

    if cfg!(feature = "hll") {
        build
            .file("cpp/hll/hll_sketch_shim.cc")
            .file("cpp/hll/hll_union_shim.cc");
    }
    if cfg!(feature = "theta") {
        for path in [
            "cpp/theta/theta_sketch_shim.cc",
            "cpp/theta/theta_compact_shim.cc",
            "cpp/theta/theta_wrapped_shim.cc",
            "cpp/theta/theta_union_shim.cc",
            "cpp/theta/theta_intersection_shim.cc",
            "cpp/theta/theta_a_not_b_shim.cc",
            "cpp/theta/theta_jaccard_shim.cc",
        ] {
            require_exists(path);
            build.file(path);
        }
    }
    if cfg!(feature = "cpc") {
        for path in ["cpp/cpc/cpc_sketch_shim.cc", "cpp/cpc/cpc_union_shim.cc"] {
            require_exists(path);
            build.file(path);
        }
    }
    if cfg!(feature = "tuple") {
        for path in [
            "cpp/tuple/array_of_doubles_sketch_shim.cc",
            "cpp/tuple/array_of_doubles_compact_shim.cc",
            "cpp/tuple/array_of_doubles_union_shim.cc",
            "cpp/tuple/array_of_doubles_intersection_shim.cc",
            "cpp/tuple/array_of_doubles_a_not_b_shim.cc",
            "cpp/tuple/array_of_doubles_jaccard_shim.cc",
            "cpp/tuple/dyn_summary.cc",
            "cpp/tuple/tuple_generic_sketch_shim.cc",
            "cpp/tuple/tuple_generic_compact_shim.cc",
            "cpp/tuple/tuple_generic_union_shim.cc",
            "cpp/tuple/tuple_generic_intersection_shim.cc",
            "cpp/tuple/tuple_generic_a_not_b_shim.cc",
            "cpp/tuple/tuple_generic_jaccard_shim.cc",
        ] {
            require_exists(path);
            build.file(path);
        }
    }

    build.compile("apache_datasketches_sys");

    println!("cargo:rerun-if-changed=src/hll.rs");
    println!("cargo:rerun-if-changed=cpp/hll/hll_sketch_shim.h");
    println!("cargo:rerun-if-changed=cpp/hll/hll_sketch_shim.cc");
    println!("cargo:rerun-if-changed=cpp/hll/hll_union_shim.h");
    println!("cargo:rerun-if-changed=cpp/hll/hll_union_shim.cc");
    println!("cargo:rerun-if-changed=src/theta_sketch.rs");
    println!("cargo:rerun-if-changed=src/theta_compact.rs");
    println!("cargo:rerun-if-changed=src/theta_wrapped.rs");
    println!("cargo:rerun-if-changed=src/theta_union.rs");
    println!("cargo:rerun-if-changed=src/theta_intersection.rs");
    println!("cargo:rerun-if-changed=src/theta_a_not_b.rs");
    println!("cargo:rerun-if-changed=src/theta_jaccard.rs");
    println!("cargo:rerun-if-changed=cpp/theta/theta_sketch_shim.h");
    println!("cargo:rerun-if-changed=cpp/theta/theta_sketch_shim.cc");
    println!("cargo:rerun-if-changed=cpp/theta/theta_compact_shim.h");
    println!("cargo:rerun-if-changed=cpp/theta/theta_compact_shim.cc");
    println!("cargo:rerun-if-changed=cpp/theta/theta_wrapped_shim.h");
    println!("cargo:rerun-if-changed=cpp/theta/theta_wrapped_shim.cc");
    println!("cargo:rerun-if-changed=cpp/theta/theta_union_shim.h");
    println!("cargo:rerun-if-changed=cpp/theta/theta_union_shim.cc");
    println!("cargo:rerun-if-changed=cpp/theta/theta_intersection_shim.h");
    println!("cargo:rerun-if-changed=cpp/theta/theta_intersection_shim.cc");
    println!("cargo:rerun-if-changed=cpp/theta/theta_a_not_b_shim.h");
    println!("cargo:rerun-if-changed=cpp/theta/theta_a_not_b_shim.cc");
    println!("cargo:rerun-if-changed=cpp/theta/theta_jaccard_shim.h");
    println!("cargo:rerun-if-changed=cpp/theta/theta_jaccard_shim.cc");
    println!("cargo:rerun-if-changed=src/cpc_sketch.rs");
    println!("cargo:rerun-if-changed=src/cpc_union.rs");
    println!("cargo:rerun-if-changed=cpp/cpc/cpc_sketch_shim.h");
    println!("cargo:rerun-if-changed=cpp/cpc/cpc_sketch_shim.cc");
    println!("cargo:rerun-if-changed=cpp/cpc/cpc_union_shim.h");
    println!("cargo:rerun-if-changed=cpp/cpc/cpc_union_shim.cc");
    println!("cargo:rerun-if-changed=src/array_of_doubles_sketch.rs");
    println!("cargo:rerun-if-changed=src/array_of_doubles_compact.rs");
    println!("cargo:rerun-if-changed=src/array_of_doubles_input.rs");
    println!("cargo:rerun-if-changed=src/array_of_doubles_union.rs");
    println!("cargo:rerun-if-changed=src/array_of_doubles_intersection.rs");
    println!("cargo:rerun-if-changed=src/array_of_doubles_a_not_b.rs");
    println!("cargo:rerun-if-changed=src/array_of_doubles_jaccard.rs");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_sketch_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_sketch_shim.cc");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_compact_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_compact_shim.cc");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_union_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_union_shim.cc");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_intersection_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_intersection_shim.cc");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_a_not_b_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_a_not_b_shim.cc");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_jaccard_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/array_of_doubles_jaccard_shim.cc");
    println!("cargo:rerun-if-changed=src/tuple_generic.rs");
    println!("cargo:rerun-if-changed=cpp/tuple/dyn_summary.h");
    println!("cargo:rerun-if-changed=cpp/tuple/dyn_summary.cc");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_sketch_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_sketch_shim.cc");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_compact_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_compact_shim.cc");
    println!("cargo:rerun-if-changed=src/tuple_generic_input.rs");
    println!("cargo:rerun-if-changed=src/tuple_generic_union.rs");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_union_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_union_shim.cc");
    println!("cargo:rerun-if-changed=src/tuple_generic_intersection.rs");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_intersection_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_intersection_shim.cc");
    println!("cargo:rerun-if-changed=src/tuple_generic_a_not_b.rs");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_a_not_b_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_a_not_b_shim.cc");
    println!("cargo:rerun-if-changed=src/tuple_generic_jaccard.rs");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_jaccard_shim.h");
    println!("cargo:rerun-if-changed=cpp/tuple/tuple_generic_jaccard_shim.cc");
}

/// Guards against a collision class that has already caused a real crash on
/// this codebase: cxx derives each generated `extern "C"` trampoline symbol
/// (for free functions) and each generated C++ type definition (for shared
/// `struct`/`enum`/opaque types) from the `#[cxx::bridge(namespace = ..)]`
/// namespace plus the item's *name* alone — not from its parameter types and
/// not from which bridge module declared it. Two bridges that happen to
/// declare a free function or shared type with the same name therefore emit
/// the identical C++ symbol; the linker silently picks one definition for
/// both call sites, and callers of the "losing" declaration get the wrong
/// shim type reinterpreted at runtime instead of a link error. This is
/// exactly what happened when the theta and tuple jaccard shims both
/// declared `jaccard_sketch_sketch` (and three siblings) in the
/// `apache_datasketches_rs` namespace, producing a SIGBUS under
/// `--all-features` (fixed by renaming the tuple side to `tuple_jaccard_*`).
///
/// Methods are *not* affected — the receiver type is part of a method's
/// trampoline symbol — so this check only tracks free functions (a `fn`
/// whose first parameter is not `self: ...`) and type *definitions*: a bare
/// `type Name;` (opaque C++ type) or a `struct Name {`/`enum Name {` inside
/// the bridge module. A cross-bridge alias of the form
/// `type Name = crate::other_module::ffi::Name;` re-uses a type already
/// defined by another bridge and emits no second C++ definition, so it must
/// not be flagged as a duplicate — only the original bare declaration counts.
///
/// This is a deliberately simple line-oriented scan over the bridge source
/// files that are actually being compiled in this build (the `bridges` list
/// above, which already reflects the active feature set), not a full Rust
/// parser. It is documented in `AGENTS.md`'s "cxx::bridge names must be
/// globally unique" section.
fn check_bridge_name_uniqueness(bridges: &[&str]) {
    use std::collections::HashMap;

    // name -> the first bridge file that defined it.
    let mut type_defs: HashMap<String, String> = HashMap::new();
    let mut fn_defs: HashMap<String, String> = HashMap::new();

    for &path in bridges {
        let content = match std::fs::read_to_string(path) {
            Ok(c) => c,
            Err(_) => continue,
        };
        let lines: Vec<&str> = content.lines().collect();
        let mut i = 0;
        let recorded_before = type_defs.len() + fn_defs.len();

        // Tracks whether the scan is currently inside the `#[cxx::bridge]`
        // module body (the mod opened right after such an attribute).
        //
        // Everything this guard cares about -- shared struct/enum
        // definitions, bare opaque `type Name;` declarations, and free
        // function declarations -- exists only *inside* a bridge module, so
        // the whole scan is gated on being inside one. That gate is also what
        // keeps two `extern "Rust"` shapes from producing false positives:
        //
        //  * The Rust type backing an opaque `type Name;` declaration is a
        //    plain `struct Name { .. }` defined elsewhere in the same file,
        //    outside the bridge module (see `RustSummary` in
        //    src/tuple_generic.rs). That plain struct is not a second
        //    C++-visible type definition -- it's the one and only
        //    implementation behind the single opaque declaration.
        //  * A trampoline is declared once inside the `extern "Rust"` block
        //    and implemented once as a plain `fn` of the same name outside
        //    the bridge module (see `rust_summary_clone` and siblings). Only
        //    the in-bridge declaration is a name cxx turns into a symbol, so
        //    ignoring `fn` definitions outside the bridge both avoids a false
        //    self-collision and removes any need to skip function bodies.
        //
        // Because the gate fails *closed* only if bridge entry is detected
        // correctly, entry detection must not depend on where rustfmt put the
        // opening brace: `saw_bridge_attr` stays set until a `{` is actually
        // seen, which may be on the `mod` line or on a following line.
        let mut brace_balance: i32 = 0;
        let mut saw_bridge_attr = false;
        let mut bridge_entry_depth: Option<i32> = None;

        while i < lines.len() {
            let trimmed = lines[i].trim();

            if trimmed.starts_with("//") {
                i += 1;
                continue;
            }

            if trimmed.starts_with("#[cxx::bridge") {
                saw_bridge_attr = true;
                i += 1;
                continue;
            }

            // Brace bookkeeping, which only runs from the bridge attribute
            // onwards. `bridge_entry_depth` records the brace balance
            // *outside* the bridge body; the bridge is exited when the
            // balance returns to it.
            if saw_bridge_attr || bridge_entry_depth.is_some() {
                let opens = trimmed.matches('{').count() as i32;
                let closes = trimmed.matches('}').count() as i32;
                if saw_bridge_attr && opens > 0 {
                    bridge_entry_depth = Some(brace_balance);
                    saw_bridge_attr = false;
                }
                brace_balance += opens - closes;
                if bridge_entry_depth == Some(brace_balance) {
                    bridge_entry_depth = None;
                }
            }

            if bridge_entry_depth.is_none() {
                i += 1;
                continue;
            }

            if let Some(name) = extract_struct_or_enum_name(trimmed) {
                record_definition(&mut type_defs, name, path, "shared struct/enum");
                i += 1;
                continue;
            }

            if let Some(rest) = trimmed.strip_prefix("type ") {
                let rest = rest.trim_end_matches(';').trim();
                if !rest.contains('=') {
                    // Bare `type Name;` — an opaque C++ type definition.
                    // (A cross-bridge alias `type Name = crate::...;`
                    // reuses another bridge's definition and is not a
                    // new one.)
                    if !rest.is_empty() {
                        record_definition(&mut type_defs, rest.to_string(), path, "opaque type");
                    }
                }
                i += 1;
                continue;
            }

            if trimmed.starts_with("fn ") {
                // Declarations inside an `extern` block can wrap across lines
                // (see e.g. `tuple_jaccard_sketch_sketch` in
                // src/array_of_doubles_jaccard.rs). Accumulate lines until
                // the parentheses balance and the statement terminates.
                let mut stmt = String::new();
                let mut paren_depth = 0i32;
                let mut seen_paren = false;
                let mut j = i;
                loop {
                    let line = lines[j];
                    stmt.push_str(line);
                    stmt.push(' ');
                    for c in line.chars() {
                        match c {
                            '(' => {
                                paren_depth += 1;
                                seen_paren = true;
                            }
                            ')' => paren_depth -= 1,
                            _ => {}
                        }
                    }
                    if seen_paren && paren_depth == 0 {
                        let end = stmt.trim_end();
                        if end.ends_with(';') || end.ends_with('{') || end.ends_with('}') {
                            break;
                        }
                    }
                    j += 1;
                    if j >= lines.len() {
                        break;
                    }
                }

                // Any braces on the continuation lines still have to be
                // accounted for, or the bridge-exit detection would drift.
                // (A bridge `fn` declaration has none in practice; this only
                // keeps the bookkeeping honest.)
                if j > i {
                    for line in &lines[i + 1..=j.min(lines.len() - 1)] {
                        brace_balance += line.matches('{').count() as i32;
                        brace_balance -= line.matches('}').count() as i32;
                    }
                    if bridge_entry_depth == Some(brace_balance) {
                        bridge_entry_depth = None;
                    }
                }

                i = j + 1;

                // Only a `;`-terminated statement is a bridge declaration.
                if stmt.trim_end().ends_with(';') {
                    if let (Some(fn_pos), Some(paren_pos)) = (stmt.find("fn "), stmt.find('(')) {
                        if paren_pos > fn_pos {
                            let name = stmt[fn_pos + 3..paren_pos].trim().to_string();
                            let params = stmt[paren_pos + 1..].trim_start();
                            let is_method = params.starts_with("self");
                            if !is_method && !name.is_empty() {
                                record_definition(&mut fn_defs, name, path, "free function");
                            }
                        }
                    }
                }
                continue;
            }

            i += 1;
        }

        // Fail CLOSED on a file the scan learned nothing from. Every bridge
        // file declares at least one bare opaque `type Name;` or shared
        // `struct`/`enum`, and all but a couple also declare free functions,
        // so a zero here never means "this bridge genuinely has no names" —
        // it means the scan lost the bridge body. Both known fail-open modes
        // (brace bookkeeping thrown off by a brace inside a block comment or
        // string literal, and a bridge whose `mod` line was reformatted so
        // entry detection missed it) present exactly this way, so this single
        // assertion closes both without teaching the line scanner about Rust
        // comments and literals.
        assert!(
            type_defs.len() + fn_defs.len() > recorded_before,
            "apache-datasketches-sys build.rs: the bridge-name scanner recorded no type \
             or free function from `{path}`.\n\n\
             Every `#[cxx::bridge]` file declares at least one opaque type, shared \
             struct/enum, or free function, so this means the scan failed to find or \
             follow the bridge module in that file -- most likely the brace bookkeeping \
             in check_bridge_name_uniqueness was thrown off (a brace inside a block \
             comment or string literal), or the `#[cxx::bridge]`/`mod` shape changed. \
             The cross-bridge name-collision check is therefore NOT covering this file, \
             which previously caused a SIGBUS. Fix the scanner (or the file) rather than \
             removing this assertion."
        );
    }
}

fn extract_struct_or_enum_name(trimmed: &str) -> Option<String> {
    let after_kw = trimmed
        .strip_prefix("pub struct ")
        .or_else(|| trimmed.strip_prefix("struct "))
        .or_else(|| trimmed.strip_prefix("pub enum "))
        .or_else(|| trimmed.strip_prefix("enum "))?;
    let name: String = after_kw
        .chars()
        .take_while(|c| c.is_alphanumeric() || *c == '_')
        .collect();
    if name.is_empty() {
        None
    } else {
        Some(name)
    }
}

fn record_definition(
    map: &mut std::collections::HashMap<String, String>,
    name: String,
    path: &str,
    kind: &str,
) {
    if let Some(existing) = map.get(&name) {
        panic!(
            "apache-datasketches-sys build.rs: duplicate {kind} name `{name}` is defined in both \
             `{existing}` and `{path}`.\n\n\
             cxx derives each generated extern \"C\" trampoline symbol (for free functions) and \
             each generated C++ type definition (for shared struct/enum/opaque types) from the \
             bridge namespace plus the item's name alone -- not from parameter types and not from \
             which bridge module declared it. Two bridges declaring the same name therefore emit \
             the identical C++ symbol, and the linker silently picks one definition for both call \
             sites: callers of the \"losing\" declaration get the wrong shim type reinterpreted at \
             runtime, which shows up as a crash or a wrong result, not a link error. This is \
             exactly the bug class that previously caused a SIGBUS when the theta and tuple \
             jaccard shims both declared `jaccard_sketch_sketch` under --all-features. Rename one \
             of the two, typically by prefixing with its family name (e.g. `tuple_jaccard_*`, \
             `TupleResizeFactor`)."
        );
    }
    map.insert(name, path.to_string());
}