Ports ZIG/src/lib_vt.zig and terminal/c/* as crates/ghostty-vt-capi (cdylib + staticlib + rlib, soname libghostty-vt.so) implementing the unchanged include/ghostty/vt.h + vt/*.h ABI: OSC parser, SGR parser, key events and key encoding, paste safety, colors, result codes - all 38 native symbols, plus the ghostty_wasm_* helpers on wasm32. Verification: - tests/abi.rs generates a C probe from the headers, compiled with cc, that checks sizeof/alignof/offsetof of every struct/union, every enumerator and #define against Rust, and links every declared symbol; nm -D exports equal the declared functions. - Ghostty's example/c-vt* programs compile unchanged against the Rust library (shared and static) and produce the expected output. - The wasm32 build runs Ghostty's wasm-key-encode/wasm-sgr example scripts unmodified under node (upstream's wasm-sgr tag table is stale vs sgr.h; a regenerated copy checks full output). Test Plan: - cargo test -p ghostty-vt-capi (22 unit + 2 ABI + 5 C + 3 wasm tests)
659 lines
20 KiB
Rust
659 lines
20 KiB
Rust
//! ABI verification: a C program generated from the unchanged headers and
|
|
//! compiled with `cc` prints `sizeof`/`alignof`/`offsetof` of every struct and
|
|
//! union, `sizeof` of every enum/typedef, the value of every enumerator and
|
|
//! `#define`d constant, and takes the address of every declared function (so
|
|
//! linking proves the symbol is exported). The values are compared against the
|
|
//! Rust definitions (`size_of`/`align_of`/`offset_of!`, enum discriminants).
|
|
#![cfg(unix)]
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
mod common;
|
|
|
|
use std::{
|
|
collections::BTreeMap,
|
|
fmt::Write as _,
|
|
mem::{align_of, offset_of, size_of},
|
|
process::Command,
|
|
};
|
|
|
|
use ghostty_input::{
|
|
Mods,
|
|
key::Key,
|
|
key_mods::{ModSide, Side},
|
|
};
|
|
use ghostty_vt::{color::Name, kitty::key::Flags as KittyFlags};
|
|
use ghostty_vt_capi::{
|
|
alloc::{Allocator, AllocatorVtable},
|
|
color::GhosttyColorRgb,
|
|
key_encode::KeyEncoderOption,
|
|
key_event::KeyAction,
|
|
osc::OscCommandData,
|
|
result::GhosttyResult,
|
|
sgr::{SgrAttribute, SgrAttributeTag, SgrAttributeValue, SgrUnknown},
|
|
};
|
|
|
|
/// `GHOSTTY_FOO_BAR` / `FooBar` / `foo_bar` -> `foobar`.
|
|
fn norm(s: &str) -> String {
|
|
s.chars()
|
|
.filter(|c| *c != '_')
|
|
.map(|c| c.to_ascii_lowercase())
|
|
.collect()
|
|
}
|
|
|
|
/// Parsed `typedef enum { ... } Name;` blocks: typedef name -> enumerators.
|
|
fn header_enums() -> BTreeMap<String, Vec<String>> {
|
|
let mut map = BTreeMap::new();
|
|
for h in common::headers() {
|
|
let text = common::strip_comments(&std::fs::read_to_string(h).unwrap());
|
|
let mut rest = text.as_str();
|
|
while let Some(start) = rest.find("typedef enum") {
|
|
let after = &rest[start..];
|
|
let open = after.find('{').unwrap();
|
|
let close = after.find('}').unwrap();
|
|
let name_end = after[close..].find(';').unwrap() + close;
|
|
let name = after[close + 1..name_end].trim().to_string();
|
|
let names = after[open + 1..close]
|
|
.split(',')
|
|
.map(|e| e.split('=').next().unwrap().trim().to_string())
|
|
.filter(|e| !e.is_empty())
|
|
.collect();
|
|
map.insert(name, names);
|
|
rest = &after[name_end..];
|
|
}
|
|
}
|
|
map
|
|
}
|
|
|
|
/// `#define GHOSTTY_X ...` constants that are not include guards.
|
|
fn header_defines() -> Vec<String> {
|
|
let mut v = Vec::new();
|
|
for h in common::headers() {
|
|
for line in std::fs::read_to_string(h).unwrap().lines() {
|
|
if let Some(rest) = line.strip_prefix("#define GHOSTTY_") {
|
|
let name = rest.split_whitespace().next().unwrap();
|
|
if !name.ends_with("_H") && rest.split_whitespace().nth(1).is_some() {
|
|
v.push(format!("GHOSTTY_{name}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
v
|
|
}
|
|
|
|
/// Names and values of a Rust-side enum, keyed by normalized name.
|
|
fn vals<T: std::fmt::Debug + Copy>(items: &[T], f: impl Fn(T) -> i64) -> BTreeMap<String, i64> {
|
|
items
|
|
.iter()
|
|
.map(|i| (norm(&format!("{i:?}")), f(*i)))
|
|
.collect()
|
|
}
|
|
|
|
struct Probe {
|
|
c: String,
|
|
expected: BTreeMap<String, i64>,
|
|
}
|
|
|
|
impl Probe {
|
|
fn emit(&mut self, key: &str, c_expr: &str, expected: i64) {
|
|
let _ = writeln!(
|
|
self.c,
|
|
" printf(\"%s=%lld\\n\", \"{key}\", (long long)({c_expr}));"
|
|
);
|
|
self.expected.insert(key.to_string(), expected);
|
|
}
|
|
|
|
fn layout(&mut self, ty: &str, size: usize, align: usize, fields: &[(&str, usize)]) {
|
|
self.emit(
|
|
&format!("sizeof({ty})"),
|
|
&format!("sizeof({ty})"),
|
|
size as i64,
|
|
);
|
|
self.emit(
|
|
&format!("alignof({ty})"),
|
|
&format!("_Alignof({ty})"),
|
|
align as i64,
|
|
);
|
|
for (f, off) in fields {
|
|
self.emit(
|
|
&format!("offsetof({ty},{f})"),
|
|
&format!("offsetof({ty}, {f})"),
|
|
*off as i64,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Check every enumerator of `typedef` against `rust` (names after
|
|
/// `prefix`); the C compiler provides the values. `exact` also requires
|
|
/// that Rust has no extra members.
|
|
fn enumerators(
|
|
&mut self,
|
|
enums: &BTreeMap<String, Vec<String>>,
|
|
typedef: &str,
|
|
prefix: &str,
|
|
rust: &BTreeMap<String, i64>,
|
|
exact: bool,
|
|
) {
|
|
let names = enums
|
|
.get(typedef)
|
|
.unwrap_or_else(|| panic!("no enum {typedef}"));
|
|
// All GHOSTTY enums are `int` sized.
|
|
self.emit(
|
|
&format!("sizeof({typedef})"),
|
|
&format!("sizeof({typedef})"),
|
|
4,
|
|
);
|
|
for n in names {
|
|
let short = n
|
|
.strip_prefix(prefix)
|
|
.unwrap_or_else(|| panic!("{n} lacks {prefix}"));
|
|
let want = *rust
|
|
.get(&norm(short))
|
|
.unwrap_or_else(|| panic!("{typedef}: Rust has no member for {n}"));
|
|
self.emit(n, n, want);
|
|
}
|
|
if exact {
|
|
assert_eq!(names.len(), rust.len(), "{typedef}: member count differs");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_probe() -> Probe {
|
|
let enums = header_enums();
|
|
let mut p = Probe {
|
|
c: String::new(),
|
|
expected: BTreeMap::new(),
|
|
};
|
|
|
|
// ---- structs and unions ----
|
|
p.layout(
|
|
"GhosttyColorRgb",
|
|
size_of::<GhosttyColorRgb>(),
|
|
align_of::<GhosttyColorRgb>(),
|
|
&[
|
|
("r", offset_of!(GhosttyColorRgb, r)),
|
|
("g", offset_of!(GhosttyColorRgb, g)),
|
|
("b", offset_of!(GhosttyColorRgb, b)),
|
|
],
|
|
);
|
|
p.layout(
|
|
"GhosttyAllocatorVtable",
|
|
size_of::<AllocatorVtable>(),
|
|
align_of::<AllocatorVtable>(),
|
|
&[
|
|
("alloc", offset_of!(AllocatorVtable, alloc)),
|
|
("resize", offset_of!(AllocatorVtable, resize)),
|
|
("remap", offset_of!(AllocatorVtable, remap)),
|
|
("free", offset_of!(AllocatorVtable, free)),
|
|
],
|
|
);
|
|
p.layout(
|
|
"GhosttyAllocator",
|
|
size_of::<Allocator>(),
|
|
align_of::<Allocator>(),
|
|
&[
|
|
("ctx", offset_of!(Allocator, ctx)),
|
|
("vtable", offset_of!(Allocator, vtable)),
|
|
],
|
|
);
|
|
p.layout(
|
|
"GhosttySgrUnknown",
|
|
size_of::<SgrUnknown>(),
|
|
align_of::<SgrUnknown>(),
|
|
&[
|
|
("full_ptr", offset_of!(SgrUnknown, full_ptr)),
|
|
("full_len", offset_of!(SgrUnknown, full_len)),
|
|
("partial_ptr", offset_of!(SgrUnknown, partial_ptr)),
|
|
("partial_len", offset_of!(SgrUnknown, partial_len)),
|
|
],
|
|
);
|
|
p.layout(
|
|
"GhosttySgrAttributeValue",
|
|
size_of::<SgrAttributeValue>(),
|
|
align_of::<SgrAttributeValue>(),
|
|
&[
|
|
("unknown", offset_of!(SgrAttributeValue, unknown)),
|
|
("underline", offset_of!(SgrAttributeValue, underline)),
|
|
(
|
|
"underline_color",
|
|
offset_of!(SgrAttributeValue, underline_color),
|
|
),
|
|
(
|
|
"underline_color_256",
|
|
offset_of!(SgrAttributeValue, underline_color_256),
|
|
),
|
|
(
|
|
"direct_color_fg",
|
|
offset_of!(SgrAttributeValue, direct_color_fg),
|
|
),
|
|
(
|
|
"direct_color_bg",
|
|
offset_of!(SgrAttributeValue, direct_color_bg),
|
|
),
|
|
("bg_8", offset_of!(SgrAttributeValue, bg_8)),
|
|
("fg_8", offset_of!(SgrAttributeValue, fg_8)),
|
|
("bright_bg_8", offset_of!(SgrAttributeValue, bright_bg_8)),
|
|
("bright_fg_8", offset_of!(SgrAttributeValue, bright_fg_8)),
|
|
("bg_256", offset_of!(SgrAttributeValue, bg_256)),
|
|
("fg_256", offset_of!(SgrAttributeValue, fg_256)),
|
|
("_padding", offset_of!(SgrAttributeValue, _padding)),
|
|
],
|
|
);
|
|
p.layout(
|
|
"GhosttySgrAttribute",
|
|
size_of::<SgrAttribute>(),
|
|
align_of::<SgrAttribute>(),
|
|
&[
|
|
("tag", offset_of!(SgrAttribute, tag)),
|
|
("value", offset_of!(SgrAttribute, value)),
|
|
],
|
|
);
|
|
|
|
// ---- scalar typedefs and opaque handles ----
|
|
for (ty, size) in [
|
|
("GhosttyMods", 2),
|
|
("GhosttyKittyKeyFlags", 1),
|
|
("GhosttyColorPaletteIndex", 1),
|
|
("GhosttyOscParser", size_of::<usize>()),
|
|
("GhosttyOscCommand", size_of::<usize>()),
|
|
("GhosttySgrParser", size_of::<usize>()),
|
|
("GhosttyKeyEvent", size_of::<usize>()),
|
|
("GhosttyKeyEncoder", size_of::<usize>()),
|
|
] {
|
|
p.emit(
|
|
&format!("sizeof({ty})"),
|
|
&format!("sizeof({ty})"),
|
|
size as i64,
|
|
);
|
|
}
|
|
|
|
// ---- enums ----
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttyResult",
|
|
"GHOSTTY_",
|
|
&vals(
|
|
&[
|
|
GhosttyResult::Success,
|
|
GhosttyResult::OutOfMemory,
|
|
GhosttyResult::InvalidValue,
|
|
],
|
|
|v| v as i64,
|
|
),
|
|
true,
|
|
);
|
|
// The Rust OSC parser knows two commands newer than the header.
|
|
let osc = {
|
|
use ghostty_vt::osc::Key::*;
|
|
vals(
|
|
&[
|
|
Invalid,
|
|
ChangeWindowTitle,
|
|
ChangeWindowIcon,
|
|
SemanticPrompt,
|
|
ClipboardContents,
|
|
ReportPwd,
|
|
MouseShape,
|
|
ColorOperation,
|
|
KittyColorProtocol,
|
|
ShowDesktopNotification,
|
|
HyperlinkStart,
|
|
HyperlinkEnd,
|
|
ConemuSleep,
|
|
ConemuShowMessageBox,
|
|
ConemuChangeTabTitle,
|
|
ConemuProgressReport,
|
|
ConemuWaitInput,
|
|
ConemuGuimacro,
|
|
ConemuRunProcess,
|
|
ConemuOutputEnvironmentVariable,
|
|
ConemuXtermEmulation,
|
|
ConemuComment,
|
|
KittyTextSizing,
|
|
KittyClipboardProtocol,
|
|
ContextSignal,
|
|
],
|
|
|v| v as i64,
|
|
)
|
|
};
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttyOscCommandType",
|
|
"GHOSTTY_OSC_COMMAND_",
|
|
&osc,
|
|
false,
|
|
);
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttyOscCommandData",
|
|
"GHOSTTY_OSC_DATA_",
|
|
&vals(
|
|
&[
|
|
OscCommandData::Invalid,
|
|
OscCommandData::ChangeWindowTitleStr,
|
|
],
|
|
|v| v as i64,
|
|
),
|
|
true,
|
|
);
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttySgrAttributeTag",
|
|
"GHOSTTY_SGR_ATTR_",
|
|
&sgr_tags(),
|
|
true,
|
|
);
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttySgrUnderline",
|
|
"GHOSTTY_SGR_UNDERLINE_",
|
|
&vals(
|
|
&[
|
|
ghostty_vt::sgr::Underline::None,
|
|
ghostty_vt::sgr::Underline::Single,
|
|
ghostty_vt::sgr::Underline::Double,
|
|
ghostty_vt::sgr::Underline::Curly,
|
|
ghostty_vt::sgr::Underline::Dotted,
|
|
ghostty_vt::sgr::Underline::Dashed,
|
|
],
|
|
|v| v as i64,
|
|
),
|
|
true,
|
|
);
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttyKeyAction",
|
|
"GHOSTTY_KEY_ACTION_",
|
|
&vals(
|
|
&[KeyAction::Release, KeyAction::Press, KeyAction::Repeat],
|
|
|v| v as i64,
|
|
),
|
|
true,
|
|
);
|
|
// ghostty_input::Action must agree with the header too.
|
|
assert_eq!(ghostty_input::Action::Release as i32, 0);
|
|
assert_eq!(ghostty_input::Action::Press as i32, 1);
|
|
assert_eq!(ghostty_input::Action::Repeat as i32, 2);
|
|
|
|
let keys: BTreeMap<String, i64> = Key::ALL
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, k)| {
|
|
let n = k.name();
|
|
(norm(n.strip_prefix("key_").unwrap_or(n)), i as i64)
|
|
})
|
|
.collect();
|
|
p.enumerators(&enums, "GhosttyKey", "GHOSTTY_KEY_", &keys, true);
|
|
for (i, k) in Key::ALL.iter().enumerate() {
|
|
assert_eq!(*k as usize, i, "Key discriminants are dense");
|
|
}
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttyKeyEncoderOption",
|
|
"GHOSTTY_KEY_ENCODER_OPT_",
|
|
&vals(
|
|
&[
|
|
KeyEncoderOption::CursorKeyApplication,
|
|
KeyEncoderOption::KeypadKeyApplication,
|
|
KeyEncoderOption::IgnoreKeypadWithNumlock,
|
|
KeyEncoderOption::AltEscPrefix,
|
|
KeyEncoderOption::ModifyOtherKeysState2,
|
|
KeyEncoderOption::KittyFlags,
|
|
KeyEncoderOption::MacosOptionAsAlt,
|
|
],
|
|
|v| v as i64,
|
|
),
|
|
true,
|
|
);
|
|
p.enumerators(
|
|
&enums,
|
|
"GhosttyOptionAsAlt",
|
|
"GHOSTTY_OPTION_AS_ALT_",
|
|
&vals(
|
|
&[
|
|
ghostty_input::OptionAsAlt::False,
|
|
ghostty_input::OptionAsAlt::True,
|
|
ghostty_input::OptionAsAlt::Left,
|
|
ghostty_input::OptionAsAlt::Right,
|
|
],
|
|
|v| v as i64,
|
|
),
|
|
true,
|
|
);
|
|
|
|
// ---- #define constants ----
|
|
let right = |f: fn(&mut Side)| {
|
|
let mut s = Side::default();
|
|
f(&mut s);
|
|
Mods {
|
|
sides: s,
|
|
..Mods::default()
|
|
}
|
|
.int() as i64
|
|
};
|
|
let _ = ModSide::Right;
|
|
let mod_flag = |f: fn(&mut Mods)| {
|
|
let mut m = Mods::default();
|
|
f(&mut m);
|
|
m.int() as i64
|
|
};
|
|
let kitty = |f: fn(&mut KittyFlags)| {
|
|
let mut k = KittyFlags::DISABLED;
|
|
f(&mut k);
|
|
k.int() as i64
|
|
};
|
|
let named = |n: Name| i64::from(n.0);
|
|
let defines: BTreeMap<&str, i64> = BTreeMap::from([
|
|
("GHOSTTY_MODS_SHIFT", mod_flag(|m| m.shift = true)),
|
|
("GHOSTTY_MODS_CTRL", mod_flag(|m| m.ctrl = true)),
|
|
("GHOSTTY_MODS_ALT", mod_flag(|m| m.alt = true)),
|
|
("GHOSTTY_MODS_SUPER", mod_flag(|m| m.super_ = true)),
|
|
("GHOSTTY_MODS_CAPS_LOCK", mod_flag(|m| m.caps_lock = true)),
|
|
("GHOSTTY_MODS_NUM_LOCK", mod_flag(|m| m.num_lock = true)),
|
|
(
|
|
"GHOSTTY_MODS_SHIFT_SIDE",
|
|
right(|s| s.shift = ModSide::Right),
|
|
),
|
|
("GHOSTTY_MODS_CTRL_SIDE", right(|s| s.ctrl = ModSide::Right)),
|
|
("GHOSTTY_MODS_ALT_SIDE", right(|s| s.alt = ModSide::Right)),
|
|
(
|
|
"GHOSTTY_MODS_SUPER_SIDE",
|
|
right(|s| s.super_ = ModSide::Right),
|
|
),
|
|
("GHOSTTY_KITTY_KEY_DISABLED", kitty(|_| {})),
|
|
(
|
|
"GHOSTTY_KITTY_KEY_DISAMBIGUATE",
|
|
kitty(|k| k.disambiguate = true),
|
|
),
|
|
(
|
|
"GHOSTTY_KITTY_KEY_REPORT_EVENTS",
|
|
kitty(|k| k.report_events = true),
|
|
),
|
|
(
|
|
"GHOSTTY_KITTY_KEY_REPORT_ALTERNATES",
|
|
kitty(|k| k.report_alternates = true),
|
|
),
|
|
(
|
|
"GHOSTTY_KITTY_KEY_REPORT_ALL",
|
|
kitty(|k| k.report_all = true),
|
|
),
|
|
(
|
|
"GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED",
|
|
kitty(|k| k.report_associated = true),
|
|
),
|
|
("GHOSTTY_KITTY_KEY_ALL", KittyFlags::TRUE.int() as i64),
|
|
("GHOSTTY_COLOR_NAMED_BLACK", named(Name::BLACK)),
|
|
("GHOSTTY_COLOR_NAMED_RED", named(Name::RED)),
|
|
("GHOSTTY_COLOR_NAMED_GREEN", named(Name::GREEN)),
|
|
("GHOSTTY_COLOR_NAMED_YELLOW", named(Name::YELLOW)),
|
|
("GHOSTTY_COLOR_NAMED_BLUE", named(Name::BLUE)),
|
|
("GHOSTTY_COLOR_NAMED_MAGENTA", named(Name::MAGENTA)),
|
|
("GHOSTTY_COLOR_NAMED_CYAN", named(Name::CYAN)),
|
|
("GHOSTTY_COLOR_NAMED_WHITE", named(Name::WHITE)),
|
|
(
|
|
"GHOSTTY_COLOR_NAMED_BRIGHT_BLACK",
|
|
named(Name::BRIGHT_BLACK),
|
|
),
|
|
("GHOSTTY_COLOR_NAMED_BRIGHT_RED", named(Name::BRIGHT_RED)),
|
|
(
|
|
"GHOSTTY_COLOR_NAMED_BRIGHT_GREEN",
|
|
named(Name::BRIGHT_GREEN),
|
|
),
|
|
(
|
|
"GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW",
|
|
named(Name::BRIGHT_YELLOW),
|
|
),
|
|
("GHOSTTY_COLOR_NAMED_BRIGHT_BLUE", named(Name::BRIGHT_BLUE)),
|
|
(
|
|
"GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA",
|
|
named(Name::BRIGHT_MAGENTA),
|
|
),
|
|
("GHOSTTY_COLOR_NAMED_BRIGHT_CYAN", named(Name::BRIGHT_CYAN)),
|
|
(
|
|
"GHOSTTY_COLOR_NAMED_BRIGHT_WHITE",
|
|
named(Name::BRIGHT_WHITE),
|
|
),
|
|
]);
|
|
let declared = header_defines();
|
|
for d in &declared {
|
|
let want = defines
|
|
.get(d.as_str())
|
|
.unwrap_or_else(|| panic!("header #define {d} has no Rust expectation"));
|
|
p.emit(d, d, *want);
|
|
}
|
|
assert_eq!(
|
|
declared.len(),
|
|
defines.len(),
|
|
"stale expectation in the define table"
|
|
);
|
|
p
|
|
}
|
|
|
|
fn sgr_tags() -> BTreeMap<String, i64> {
|
|
use SgrAttributeTag::*;
|
|
vals(
|
|
&[
|
|
Unset,
|
|
Unknown,
|
|
Bold,
|
|
ResetBold,
|
|
Italic,
|
|
ResetItalic,
|
|
Faint,
|
|
Underline,
|
|
UnderlineColor,
|
|
UnderlineColor256,
|
|
ResetUnderlineColor,
|
|
Overline,
|
|
ResetOverline,
|
|
Blink,
|
|
ResetBlink,
|
|
Inverse,
|
|
ResetInverse,
|
|
Invisible,
|
|
ResetInvisible,
|
|
Strikethrough,
|
|
ResetStrikethrough,
|
|
DirectColorFg,
|
|
DirectColorBg,
|
|
Bg8,
|
|
Fg8,
|
|
ResetFg,
|
|
ResetBg,
|
|
BrightBg8,
|
|
BrightFg8,
|
|
Bg256,
|
|
Fg256,
|
|
],
|
|
|v| v as i64,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn abi_matches_headers() {
|
|
let lib = common::install_native_lib();
|
|
let scratch = common::scratch_dir("abi");
|
|
let mut probe = build_probe();
|
|
|
|
let fns: Vec<String> = common::declared_functions()
|
|
.into_iter()
|
|
.filter(|f| !f.starts_with("ghostty_wasm_"))
|
|
.collect();
|
|
assert!(fns.len() == 38, "expected the full API, got {fns:?}");
|
|
let mut src = String::from(
|
|
"#include <stdio.h>\n#include <stddef.h>\n#include <stdalign.h>\n#include <ghostty/vt.h>\n",
|
|
);
|
|
src.push_str("static void *const fns[] = {\n");
|
|
for f in &fns {
|
|
let _ = writeln!(src, " (void *){f},");
|
|
}
|
|
src.push_str("};\nint main(void) {\n if (sizeof(fns) == 0) return 2;\n");
|
|
src.push_str(&probe.c);
|
|
src.push_str(" return 0;\n}\n");
|
|
|
|
let c_file = scratch.join("probe.c");
|
|
std::fs::write(&c_file, src).unwrap();
|
|
let exe = scratch.join("probe");
|
|
common::compile_c(&lib, &[&c_file], &exe, &["-Wall", "-Werror", "-std=c11"]);
|
|
let out = Command::new(&exe).output().unwrap();
|
|
assert!(out.status.success());
|
|
|
|
let got: BTreeMap<String, i64> = String::from_utf8(out.stdout)
|
|
.unwrap()
|
|
.lines()
|
|
.map(|l| {
|
|
let (k, v) = l.rsplit_once('=').unwrap();
|
|
(k.to_string(), v.parse().unwrap())
|
|
})
|
|
.collect();
|
|
probe.expected.insert("_".into(), 0);
|
|
let mut bad = Vec::new();
|
|
for (k, want) in &probe.expected {
|
|
if k == "_" {
|
|
continue;
|
|
}
|
|
match got.get(k) {
|
|
Some(g) if g == want => {}
|
|
g => bad.push(format!("{k}: C={g:?} Rust={want}")),
|
|
}
|
|
}
|
|
assert!(bad.is_empty(), "ABI mismatches:\n{}", bad.join("\n"));
|
|
assert!(got.len() > 300, "suspiciously few probes: {}", got.len());
|
|
}
|
|
|
|
/// Every exported function in the shared object is declared in the headers
|
|
/// (no accidental extra `ghostty_*` symbols on native targets).
|
|
#[test]
|
|
fn exported_symbols_match_headers() {
|
|
let lib = common::install_native_lib();
|
|
let out = Command::new("nm")
|
|
.args(["-D", "--defined-only"])
|
|
.arg(lib.join("libghostty-vt.so"))
|
|
.output();
|
|
let Ok(out) = out else {
|
|
eprintln!("nm not available; skipping");
|
|
return;
|
|
};
|
|
let text = String::from_utf8(out.stdout).unwrap();
|
|
let exported: Vec<&str> = text
|
|
.lines()
|
|
.filter_map(|l| l.split_whitespace().last())
|
|
.filter(|s| s.starts_with("ghostty_"))
|
|
.collect();
|
|
let declared: Vec<String> = common::declared_functions()
|
|
.into_iter()
|
|
.filter(|f| !f.starts_with("ghostty_wasm_"))
|
|
.collect();
|
|
for f in &declared {
|
|
assert!(
|
|
exported.contains(&f.as_str()),
|
|
"{f} declared but not exported"
|
|
);
|
|
}
|
|
for e in &exported {
|
|
assert!(
|
|
declared.iter().any(|d| d == e),
|
|
"{e} exported but not declared"
|
|
);
|
|
}
|
|
}
|