htoprs 0.4.1

A faithful Rust port of htop — the interactive process viewer
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
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
//! Partial port of `DynamicMeter.c` — htop's platform-provided dynamic
//! meter registry (name → `DynamicMeter`, rendered via the `Platform_*`
//! hooks).
//!
//! C names are preserved verbatim (htop uses `CamelCase_snake`), so
//! `non_snake_case` is allowed for the whole module — matching the spec
//! name-for-name is the point of the port. Each C function taking a
//! `Meter*`/`Hashtable*` ports to a free fn (the shape `Vector.c`/
//! `History.c` use), not a method.
//!
//! Ported (self-contained, no unported substrate):
//! - `DynamicMeter_compare` (`DynamicMeter.c:56`) — `static` in C; the
//!   `Hashtable_foreach` comparison callback. Its logic is a plain
//!   `String_eq` name match (`strcmp == 0`) plus an iterator update,
//!   needing no unported substrate.
//! - `DynamicMeter_search` (`DynamicMeter.c:65`) — drives
//!   `Hashtable_foreach(dynamics, DynamicMeter_compare, &iter)` over the
//!   registry, writing the matched key through the out-param. Both
//!   `Hashtable_foreach` and the callback ([`DynamicMeter_compare`]) are
//!   ported now (`hashtable.rs`).
//! - `DynamicMeter_lookup` (`DynamicMeter.c:74`) — thin wrapper over
//!   `Hashtable_get`, returning the stored meter's `name`; ported now.
//! - `DynamicMeter_getCaption` (`DynamicMeter.c:92`) — `static` in C; reads
//!   `this->host->settings->dynamicMeters` via `Hashtable_get` and returns
//!   the entry's `caption`/`name` (else `this->caption`). Ported now —
//!   `Settings.dynamicMeters` and `Hashtable_get` are present.
//! - `DynamicMeter_getUiName` (`DynamicMeter.c:100`) — `static` in C; same
//!   `Hashtable_get` off `settings->dynamicMeters`, then `String_safeStrncpy`
//!   copies the caption (minus a trailing `": "`) or name into the
//!   caller-provided buffer. Ported now.
//!
//! Now ported (was stubbed) — thin wrappers over the per-platform
//! `Platform_dynamicMeter*` hooks, which are `static inline` no-ops in the
//! non-PCP build (`linux/Platform.h:128-138`):
//! - `DynamicMeters_new` (`DynamicMeter.c:39`) — returns
//!   `Platform_dynamicMeters()` (null `*mut Hashtable` in this build).
//! - `DynamicMeters_delete` (`DynamicMeter.c:43`) — calls
//!   `Platform_dynamicMetersDone` then `Hashtable_delete`.
//! - `DynamicMeter_init` (`DynamicMeter.c:79`) — `static` in C; thin
//!   wrapper over `Platform_dynamicMeterInit(meter)`.
//! - `DynamicMeter_updateValues` (`DynamicMeter.c:83`) — `static` in C;
//!   thin wrapper over `Platform_dynamicMeterUpdateValues(meter)`.
//! - `DynamicMeter_display` (`DynamicMeter.c:87`) — `static` in C; thin
//!   wrapper over `Platform_dynamicMeterDisplay(meter, out)`.
//!
//! `DynamicMeter_class` (the `MeterClass` vtable literal,
//! `DynamicMeter.c:119`) and its `DynamicMeter_attributes[]` colour table
//! (`DynamicMeter.c:27`) are not ported: `MeterClass` is the C
//! function-pointer dispatch table, which the port models via Rust's own
//! per-meter modules rather than a data literal of raw fn pointers.
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)] // preserve the C-style class name `DynamicMeter_class`
#![allow(dead_code)]

use crate::ported::hashtable::{Hashtable, Hashtable_delete, Hashtable_foreach, Hashtable_get};
use crate::ported::meter::Meter;
use crate::ported::object::{Object, ObjectClass, Object_class};
use crate::ported::richstring::RichString;
use crate::ported::xutils::String_safeStrncpy;

#[cfg(target_os = "macos")]
use crate::ported::darwin::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};
#[cfg(target_os = "dragonfly")]
use crate::ported::dragonflybsd::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};
#[cfg(target_os = "freebsd")]
use crate::ported::freebsd::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};
#[cfg(target_os = "linux")]
use crate::ported::linux::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};
#[cfg(target_os = "netbsd")]
use crate::ported::netbsd::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};
#[cfg(target_os = "openbsd")]
use crate::ported::openbsd::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
use crate::ported::solaris::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};
#[cfg(not(any(
    target_os = "macos",
    target_os = "linux",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "solaris",
    target_os = "illumos",
    target_os = "dragonfly"
)))]
use crate::ported::unsupported::platform::{
    Platform_dynamicMeterDisplay, Platform_dynamicMeterInit, Platform_dynamicMeterUpdateValues,
    Platform_dynamicMeters, Platform_dynamicMetersDone,
};

/// Subset of htop's `DynamicMeter` struct (`DynamicMeter.h:17`).
///
/// `name` is the field `DynamicMeter_compare`, `DynamicMeter_search`, and
/// `DynamicMeter_lookup` touch; `caption` is read by
/// `DynamicMeter_getCaption`/`getUiName`. The C struct also carries
/// `description`, `type`, and `maximum`, modelled here for completeness.
#[derive(Debug, Clone, PartialEq)]
pub struct DynamicMeter {
    /// C: `char name[32]` — unique name, cannot contain spaces.
    pub name: String,
    /// C `char* caption` — short label shown before the meter value
    /// (`NULL` ⇒ `None`).
    pub caption: Option<String>,
    /// C `char* description` — longer explanatory text for the setup list
    /// (`NULL` ⇒ `None`).
    pub description: Option<String>,
    /// C `unsigned int type` — the meter display type id.
    pub type_: u32,
    /// C `double maximum` — the meter's value ceiling.
    pub maximum: f64,
}

/// Class descriptor for [`DynamicMeter`], present solely so a
/// `DynamicMeter` can be stored as a `Box<dyn Object>` value in the
/// ported [`Hashtable`] (whose value type is `dyn Object`). htop's
/// `Hashtable` stores raw `void*`, so C's `DynamicMeter` is **not** an
/// `Object` subclass — there is no `DynamicMeter_class` object vtable in
/// htop (the real `DynamicMeter_class` is a `MeterClass`, deliberately not
/// ported per the module note); this exists only as the safe-Rust adapter
/// for the ported table's owned `dyn Object` value model. Rooted at
/// [`Object_class`]; it sets no `display`/`compare` slots (the table never
/// dispatches through them).
static DynamicMeter_objectClass: ObjectClass = ObjectClass {
    extends: Some(&Object_class),
};

impl Object for DynamicMeter {
    fn klass(&self) -> &'static ObjectClass {
        &DynamicMeter_objectClass
    }
}

/// Port of the file-local `DynamicIterator` struct (`DynamicMeter.c:50`).
///
/// `key` is `ht_key_t`, i.e. `unsigned int` (`Hashtable.h:14`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DynamicIterator {
    pub name: String,
    pub key: u32,
    pub found: bool,
}

/// Port of `DynamicMeter.c:56`.
///
/// `Hashtable_foreach` callback: `value` is the stored `DynamicMeter*`,
/// `data` is the `DynamicIterator*`. `String_eq(a, b)` is
/// `strcmp(a, b) == 0` (`XUtils.h:60`), i.e. exact string equality here.
/// Faithful to the C: on a match it sets `found` and records `key`, with
/// no early-out — a later matching key overwrites an earlier one.
pub fn DynamicMeter_compare(key: u32, meter: &DynamicMeter, iter: &mut DynamicIterator) {
    if iter.name == meter.name {
        iter.found = true;
        iter.key = key;
    }
}

/// Port of `Hashtable* DynamicMeters_new(void)` from `DynamicMeter.c:39`.
/// Thin wrapper returning [`Platform_dynamicMeters`]; the non-PCP build's
/// `static inline` returns `NULL`, so this yields a null `*mut Hashtable`
/// (the C `Hashtable*` return maps to a raw pointer, matching
/// `Settings.dynamicMeters: Option<*mut Hashtable>`).
pub fn DynamicMeters_new() -> *mut Hashtable {
    Platform_dynamicMeters()
}

/// Port of `void DynamicMeters_delete(Hashtable* dynamics)` from
/// `DynamicMeter.c:43`. Null-guards the pointer (C `if (dynamics)`), runs the
/// platform teardown ([`Platform_dynamicMetersDone`], a non-PCP no-op), then
/// frees the table. C's `Hashtable_delete(dynamics)` takes ownership of the
/// heap `Hashtable*` and frees it; the ported [`Hashtable_delete`] consumes a
/// `Hashtable` by value, so the raw pointer is reclaimed with
/// `Box::from_raw` — the faithful analog of C's `free(dynamics)`. In the
/// non-PCP build `dynamics` is always null, so the guarded body never runs.
pub fn DynamicMeters_delete(dynamics: *mut Hashtable) {
    // C: if (dynamics) {
    if !dynamics.is_null() {
        // C: Platform_dynamicMetersDone(dynamics);
        Platform_dynamicMetersDone(dynamics);
        // C: Hashtable_delete(dynamics);
        // SAFETY: a non-null `dynamics` is a heap table owned by this call
        // (C's `Hashtable*`); reclaim it and hand it to Hashtable_delete by
        // value, which drops it (C's `free`).
        Hashtable_delete(unsafe { *Box::from_raw(dynamics) });
    }
}

/// Port of `bool DynamicMeter_search(Hashtable* dynamics, const char* name,
/// ht_key_t* key)` from `DynamicMeter.c:65`. Drives
/// [`Hashtable_foreach`] with the [`DynamicMeter_compare`] callback over the
/// registry, then writes the matched key through `key` when the out-param
/// is non-null (C `if (key) *key = iter.key`) and returns whether any entry
/// matched. A null table is skipped (C `if (dynamics)`), leaving `key`
/// unwritten from its `0` init and returning `false`.
///
/// The ported [`Hashtable_foreach`] hands each value to the callback as a
/// `&dyn Object`; C casts its `void*` straight to `const DynamicMeter*`, so
/// the safe-Rust analog downcasts through `Any` (the same round-trip the
/// stored `Box<dyn Object>` supports). Because `DynamicMeter_compare` only
/// records the `Copy` key and a `found` flag — never a borrow that must
/// escape — the single mutable `DynamicIterator` threads straight through
/// the closure, matching C's shared `&iter`.
pub fn DynamicMeter_search(
    dynamics: Option<&Hashtable>,
    name: &str,
    key: Option<&mut u32>,
) -> bool {
    // C: DynamicIterator iter = { .key = 0, .name = name, .found = false };
    let mut iter = DynamicIterator {
        name: name.to_string(),
        key: 0,
        found: false,
    };

    if let Some(dynamics) = dynamics {
        Hashtable_foreach(dynamics, &mut |k, value| {
            // C: const DynamicMeter* meter = (const DynamicMeter*)value;
            let any: &dyn core::any::Any = value;
            let meter = any
                .downcast_ref::<DynamicMeter>()
                .expect("DynamicMeter_search: hashtable value is not a DynamicMeter");
            DynamicMeter_compare(k, meter, &mut iter);
        });
    }

    // C: if (key) *key = iter.key;
    if let Some(key) = key {
        *key = iter.key;
    }

    // C: return iter.found;
    iter.found
}

/// Port of `const char* DynamicMeter_lookup(Hashtable* dynamics, ht_key_t key)`
/// from `DynamicMeter.c:74`. Thin wrapper over [`Hashtable_get`]: C casts
/// the returned `void*` to `const DynamicMeter*` and yields
/// `meter ? meter->name : NULL`; the safe-Rust analog downcasts the
/// `&dyn Object` value through `Any` and returns its `name` (or `None` on a
/// miss, C `NULL`).
pub fn DynamicMeter_lookup(dynamics: &Hashtable, key: u32) -> Option<&str> {
    Hashtable_get(dynamics, key)
        .and_then(|o| {
            let any: &dyn core::any::Any = o;
            any.downcast_ref::<DynamicMeter>()
        })
        .map(|meter| meter.name.as_str())
}

/// Port of `static void DynamicMeter_init(Meter* meter)` from
/// `DynamicMeter.c:79`. Thin wrapper over [`Platform_dynamicMeterInit`] (a
/// non-PCP no-op).
pub fn DynamicMeter_init(meter: &mut Meter) {
    Platform_dynamicMeterInit(meter);
}

/// Port of `static void DynamicMeter_updateValues(Meter* meter)` from
/// `DynamicMeter.c:83`. Thin wrapper over
/// [`Platform_dynamicMeterUpdateValues`] (a non-PCP no-op).
pub fn DynamicMeter_updateValues(meter: &mut Meter) {
    Platform_dynamicMeterUpdateValues(meter);
}

/// Port of `static void DynamicMeter_display(const Object* cast, RichString* out)`
/// from `DynamicMeter.c:87`. C casts the `Object*` to `const Meter*` and
/// forwards to [`Platform_dynamicMeterDisplay`] (a non-PCP no-op); the port
/// takes the already-typed `meter: &Meter` receiver directly — the sibling
/// panel/meter display-port convention — so the C upcast+downcast collapses
/// to the concrete reference.
pub fn DynamicMeter_display(meter: &Meter, out: &mut RichString) {
    // C: const Meter* meter = (const Meter*)cast;
    Platform_dynamicMeterDisplay(meter, out);
}

/// Port of `static const char* DynamicMeter_getCaption(const Meter* this)`
/// from `DynamicMeter.c:92`. Looks up
/// `this->host->settings->dynamicMeters` via [`Hashtable_get`] keyed on
/// `this->param` and returns `meter->caption ? meter->caption : meter->name`,
/// falling back to `this->caption` on a miss.
///
/// C reaches the registry through raw back-pointers (`this->host` is a
/// `*const Machine`, `settings->dynamicMeters` a `Hashtable*`); the port
/// mirrors that with `unsafe` derefs, and — like C — dereferences both
/// unconditionally (a NULL there is a C segfault, here an `expect` panic,
/// matching the `host->settings` precedent in `action.rs`). The stored
/// value comes back as `&dyn Object`; C casts its `void*` to
/// `const DynamicMeter*`, so the safe-Rust analog downcasts through `Any`.
/// The returned `&str` borrows `this` (its elided lifetime), the natural
/// analog of C's returned `const char*`.
pub fn DynamicMeter_getCaption(this: &Meter) -> &str {
    // C: const Settings* settings = this->host->settings;
    let settings = unsafe { (*this.host).settings.as_ref() }
        .expect("DynamicMeter_getCaption: host->settings is NULL");

    // C: const DynamicMeter* meter = Hashtable_get(settings->dynamicMeters, this->param);
    let dynamics: &Hashtable = unsafe {
        &*settings
            .dynamicMeters
            .expect("DynamicMeter_getCaption: settings->dynamicMeters is NULL")
    };
    let meter = Hashtable_get(dynamics, this.param).and_then(|o| {
        let any: &dyn core::any::Any = o;
        any.downcast_ref::<DynamicMeter>()
    });

    // C: if (meter) return meter->caption ? meter->caption : meter->name;
    if let Some(meter) = meter {
        return meter.caption.as_deref().unwrap_or(meter.name.as_str());
    }

    // C: return this->caption;
    &this.caption
}

/// Port of `static void DynamicMeter_getUiName(const Meter* this, char* name, size_t length)`
/// from `DynamicMeter.c:100`. Same `Hashtable_get` off
/// `settings->dynamicMeters` as [`DynamicMeter_getCaption`], then copies
/// the caption — minus a trailing `": "` (colon+space) — or, when the
/// entry has no caption, the name, into the caller-provided buffer via
/// [`String_safeStrncpy`]. On a registry miss (C `meter == NULL`) nothing
/// is written, leaving `name` as the caller passed it.
///
/// The C signature is `(char* name, size_t length)`: `name` is the
/// destination buffer and `length` its capacity. The port keeps that shape
/// (`name: &mut [u8]`, `length: usize`) rather than the `MeterGetUiName =
/// fn(&Meter) -> String` vtable shape, since this fn copies through
/// `String_safeStrncpy` whose `dest.len()` plays the C `size` arg — so the
/// sized copies pass `&mut name[..size]` sub-slices. C's `strlen` is a byte
/// count and its `MINIMUM` macro is `a < b ? a : b`, ported as
/// `bytes.len()` / `usize::min` (an inlined macro, not a new depth-0 fn).
pub fn DynamicMeter_getUiName(this: &Meter, name: &mut [u8], length: usize) {
    // C: assert(length > 0);
    assert!(length > 0);

    // C: const Settings* settings = this->host->settings;
    let settings = unsafe { (*this.host).settings.as_ref() }
        .expect("DynamicMeter_getUiName: host->settings is NULL");

    // C: const DynamicMeter* meter = Hashtable_get(settings->dynamicMeters, this->param);
    let dynamics: &Hashtable = unsafe {
        &*settings
            .dynamicMeters
            .expect("DynamicMeter_getUiName: settings->dynamicMeters is NULL")
    };
    let meter = Hashtable_get(dynamics, this.param).and_then(|o| {
        let any: &dyn core::any::Any = o;
        any.downcast_ref::<DynamicMeter>()
    });

    // C: if (meter) {
    if let Some(meter) = meter {
        // C: const char* uiName = meter->caption;
        if let Some(uiName) = meter.caption.as_deref() {
            let uiName = uiName.as_bytes();
            // C: size_t uiNameLen = strlen(uiName);
            let mut uiNameLen = uiName.len();
            // C: if (uiNameLen > 2 && uiName[uiNameLen - 2] == ':') uiNameLen -= 2;
            if uiNameLen > 2 && uiName[uiNameLen - 2] == b':' {
                uiNameLen -= 2;
            }
            // C: String_safeStrncpy(name, uiName, MINIMUM(length, uiNameLen + 1));
            let size = length.min(uiNameLen + 1);
            String_safeStrncpy(&mut name[..size], uiName);
        } else {
            // C: String_safeStrncpy(name, meter->name, length);
            String_safeStrncpy(&mut name[..length], meter.name.as_bytes());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn meter(name: &str) -> DynamicMeter {
        DynamicMeter {
            name: name.to_string(),
            caption: None,
            description: None,
            type_: 0,
            maximum: 0.0,
        }
    }

    fn iter(name: &str) -> DynamicIterator {
        DynamicIterator {
            name: name.to_string(),
            key: 0,
            found: false,
        }
    }

    #[test]
    fn match_sets_found_and_key() {
        let mut it = iter("cpu");
        DynamicMeter_compare(7, &meter("cpu"), &mut it);
        assert!(it.found);
        assert_eq!(it.key, 7);
    }

    #[test]
    fn no_match_leaves_iterator_untouched() {
        let mut it = iter("cpu");
        DynamicMeter_compare(7, &meter("mem"), &mut it);
        assert!(!it.found);
        assert_eq!(it.key, 0);
    }

    #[test]
    fn string_eq_is_exact_case_sensitive() {
        // String_eq == strcmp==0: case-sensitive, no trimming.
        let mut it = iter("CPU");
        DynamicMeter_compare(3, &meter("cpu"), &mut it);
        assert!(!it.found);
        assert_eq!(it.key, 0);
    }

    #[test]
    fn later_match_overwrites_key_no_early_out() {
        // C has no early return: iterating multiple matching entries
        // leaves `key` at the last one visited.
        let mut it = iter("cpu");
        DynamicMeter_compare(1, &meter("cpu"), &mut it);
        assert_eq!(it.key, 1);
        DynamicMeter_compare(2, &meter("cpu"), &mut it);
        assert!(it.found);
        assert_eq!(it.key, 2);
    }

    #[test]
    fn empty_names_compare_equal() {
        let mut it = iter("");
        DynamicMeter_compare(5, &meter(""), &mut it);
        assert!(it.found);
        assert_eq!(it.key, 5);
    }

    use crate::ported::hashtable::{Hashtable_new, Hashtable_put};

    // Build a registry the way DynamicMeters_new would: a non-owner table
    // keyed by meter index, values boxed as `dyn Object`.
    fn registry(names: &[(u32, &str)]) -> Hashtable {
        let mut ht = Hashtable_new(0, false);
        for &(k, n) in names {
            Hashtable_put(&mut ht, k, Box::new(meter(n)));
        }
        ht
    }

    #[test]
    fn lookup_returns_name_for_present_key() {
        let ht = registry(&[(1, "cpu"), (2, "mem"), (100, "io")]);
        assert_eq!(DynamicMeter_lookup(&ht, 1), Some("cpu"));
        assert_eq!(DynamicMeter_lookup(&ht, 2), Some("mem"));
        assert_eq!(DynamicMeter_lookup(&ht, 100), Some("io"));
    }

    #[test]
    fn lookup_returns_none_for_absent_key() {
        let ht = registry(&[(1, "cpu")]);
        assert_eq!(DynamicMeter_lookup(&ht, 999), None);
    }

    #[test]
    fn search_finds_by_name_and_writes_key() {
        let ht = registry(&[(10, "cpu"), (20, "mem"), (30, "io")]);
        let mut key: u32 = 0;
        assert!(DynamicMeter_search(Some(&ht), "mem", Some(&mut key)));
        assert_eq!(key, 20);
    }

    #[test]
    fn search_key_out_param_is_optional() {
        // C: `if (key)` — passing None (C NULL) must not fault.
        let ht = registry(&[(5, "cpu")]);
        assert!(DynamicMeter_search(Some(&ht), "cpu", None));
    }

    #[test]
    fn search_miss_returns_false_and_zeroes_key() {
        let ht = registry(&[(1, "cpu"), (2, "mem")]);
        let mut key: u32 = 12345;
        assert!(!DynamicMeter_search(
            Some(&ht),
            "nonexistent",
            Some(&mut key)
        ));
        // C leaves iter.key at its 0 init when nothing matches.
        assert_eq!(key, 0);
    }

    #[test]
    fn search_null_table_returns_false() {
        // C: `if (dynamics)` guards the foreach; a null table yields
        // iter.found == false and iter.key == 0.
        let mut key: u32 = 7;
        assert!(!DynamicMeter_search(None, "cpu", Some(&mut key)));
        assert_eq!(key, 0);
    }

    #[test]
    fn search_is_case_sensitive_like_string_eq() {
        // String_eq is strcmp==0: "CPU" != "cpu".
        let ht = registry(&[(1, "CPU")]);
        let mut key: u32 = 0;
        assert!(!DynamicMeter_search(Some(&ht), "cpu", Some(&mut key)));
        assert_eq!(key, 0);
        assert!(DynamicMeter_search(Some(&ht), "CPU", None));
    }

    #[test]
    fn search_result_matches_lookup_of_returned_key() {
        // The key written by search must round-trip through lookup to the
        // same meter name (the foreach/get paths must agree).
        let ht = registry(&[(3, "alpha"), (17, "beta"), (42, "gamma")]);
        for name in ["alpha", "beta", "gamma"] {
            let mut key: u32 = 0;
            assert!(DynamicMeter_search(Some(&ht), name, Some(&mut key)));
            assert_eq!(DynamicMeter_lookup(&ht, key), Some(name));
        }
    }
}