use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use super::ir::{
Abi, Const, ConstValue, Enum, Field, Function, Header, Item, Record, RecordBody, RecordKind,
Type, Typedef, Var,
};
#[derive(Debug, Clone, Default)]
pub struct Options {
pub struct_name: String,
pub library_names: Vec<String>,
pub optional: Vec<String>,
pub safe: Vec<String>,
pub comment: Vec<String>,
pub no_module_attributes: bool,
}
pub const ALLOW_ATTRIBUTES: &str = "#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals, dead_code, clippy::all, rustdoc::broken_intra_doc_links, rustdoc::invalid_html_tags, rustdoc::bare_urls)]";
const KEYWORDS: &[&str] = &[
"as", "async", "await", "box", "break", "const", "continue", "crate", "dyn", "else", "enum",
"extern", "false", "fn", "for", "gen", "if", "impl", "in", "let", "loop", "match", "mod",
"move", "mut", "priv", "pub", "ref", "return", "self", "Self", "static", "struct", "super",
"trait", "true", "try", "type", "unsafe", "use", "where", "while", "yield", "abstract",
"become", "do", "final", "macro", "override", "typeof", "unsized", "virtual",
];
pub fn emit(header: &Header, opts: &Options) -> String {
let mut e = Emitter::new(header);
e.out
.push_str("// @generated by dlsym-gen. Do not edit by hand.\n");
for line in &opts.comment {
let _ = writeln!(e.out, "// {line}");
}
if !opts.no_module_attributes {
e.out.push_str(
"\n#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals, dead_code)]\n\
#![allow(clippy::all, rustdoc::broken_intra_doc_links, rustdoc::invalid_html_tags, rustdoc::bare_urls)]\n",
);
}
let mut functions = Vec::new();
for item in &header.items {
match item {
Item::Typedef(t) => e.typedef(t),
Item::Record(r) => e.record(r),
Item::Enum(en) => e.enumeration(en),
Item::Const(c) => e.constant(c),
Item::Function(_) | Item::Var(_) => functions.push(item),
}
}
e.library(&functions, opts);
e.out
}
struct Emitter<'h> {
out: String,
enums: HashMap<&'h str, &'h Type>,
typedefs: HashMap<&'h str, &'h Type>,
unions: HashSet<&'h str>,
}
impl<'h> Emitter<'h> {
fn new(header: &'h Header) -> Self {
let mut e = Emitter {
out: String::new(),
enums: HashMap::new(),
typedefs: HashMap::new(),
unions: HashSet::new(),
};
for item in &header.items {
match item {
Item::Enum(Enum {
name: Some(n),
repr,
..
}) => {
e.enums.insert(n, repr);
}
Item::Typedef(t) => {
e.typedefs.insert(&t.name, &t.ty);
}
Item::Record(r) if r.kind == RecordKind::Union => {
e.unions.insert(&r.name);
}
_ => {}
}
}
e
}
fn resolve<'a>(&'a self, mut t: &'a Type) -> &'a Type {
for _ in 0..64 {
match t {
Type::Named(n) => match self.typedefs.get(n.as_str()) {
Some(next) => t = next,
None => return t,
},
_ => return t,
}
}
t
}
fn doc(&mut self, doc: Option<&str>, indent: &str) {
if let Some(doc) = doc {
for line in sanitize_doc(doc).lines() {
if line.is_empty() {
let _ = writeln!(self.out, "{indent}///");
} else {
let _ = writeln!(self.out, "{indent}/// {line}");
}
}
}
}
fn typedef(&mut self, t: &Typedef) {
self.out.push('\n');
self.doc(t.doc.as_deref(), "");
let _ = writeln!(
self.out,
"pub type {} = {};",
ident(&t.name),
rust_type(&t.ty)
);
}
fn enumeration(&mut self, en: &Enum) {
self.out.push('\n');
let repr = rust_type(&en.repr);
let ty = match &en.name {
Some(name) => {
let name = ident(name);
self.doc(en.doc.as_deref(), "");
let _ = writeln!(
self.out,
"#[repr(transparent)]\n\
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]\n\
pub struct {name}(pub {repr});"
);
Some(name)
}
None => None,
};
for v in &en.variants {
self.doc(v.doc.as_deref(), "");
match &ty {
Some(name) => {
let _ = writeln!(
self.out,
"pub const {}: {name} = {name}({});",
ident(&v.name),
v.value
);
}
None => {
let _ = writeln!(
self.out,
"pub const {}: {repr} = {};",
ident(&v.name),
v.value
);
}
}
}
}
fn constant(&mut self, c: &Const) {
let name = ident(&c.name);
let line = match &c.value {
ConstValue::Int(v, Type::Bool) => format!("pub const {name}: bool = {};", *v != 0),
ConstValue::Int(v, ty) => format!("pub const {name}: {} = {v};", rust_type(ty)),
ConstValue::Float(v, ty) => {
let (t, lit) = match ty {
Type::Float => ("f32", float_literal(*v as f32 as f64, "f32")),
_ => ("f64", float_literal(*v, "f64")),
};
format!("pub const {name}: {t} = {lit};")
}
ConstValue::Str(bytes) => {
format!(
"pub const {name}: &::core::ffi::CStr = {};",
c_string_literal(bytes)
)
}
};
self.out.push('\n');
self.doc(c.doc.as_deref(), "");
self.out.push_str(&line);
self.out.push('\n');
}
fn record(&mut self, r: &Record) {
let name = ident(&r.name);
self.out.push('\n');
self.doc(r.doc.as_deref(), "");
let Some(body) = &r.body else {
let _ = writeln!(
self.out,
"#[repr(C)]\n\
#[derive(Debug)]\n\
pub struct {name} {{\n \
_private: [u8; 0],\n \
_marker: ::core::marker::PhantomData<(*mut u8, ::core::marker::PhantomPinned)>,\n\
}}"
);
return;
};
let layout = match r.kind {
RecordKind::Struct => struct_layout(body),
RecordKind::Union => union_layout(body),
};
let Some(layout) = layout else {
let _ = writeln!(
self.out,
"#[repr(C, align({}))]\n\
#[derive(Clone, Copy, Debug)]\n\
pub struct {name} {{\n \
_opaque: [u8; {}],\n\
}}",
body.align, body.size
);
self.zeroed_default(&name);
self.layout_asserts(&name, body, &[]);
return;
};
let keyword = match r.kind {
RecordKind::Struct => "struct",
RecordKind::Union => "union",
};
let derive = match r.kind {
RecordKind::Struct => "Clone, Copy, Debug",
RecordKind::Union => "Clone, Copy",
};
let _ = writeln!(
self.out,
"#[repr({})]\n#[derive({derive})]\npub {keyword} {name} {{",
layout.repr
);
for slot in &layout.slots {
match slot {
Slot::Field(f) => {
self.doc(f.doc.as_deref(), " ");
let _ = writeln!(
self.out,
" pub {}: {},",
ident(&f.name),
rust_type(&f.ty)
);
}
Slot::Padding(i, n) => {
let _ = writeln!(self.out, " _pad{i}: [u8; {n}],");
}
Slot::Bitfield(g) => {
let _ = writeln!(self.out, " _bitfield{}: [u8; {}],", g.index, g.len);
}
}
}
self.out.push_str("}\n");
if r.kind == RecordKind::Union {
let _ = writeln!(
self.out,
"\nimpl ::core::fmt::Debug for {name} {{\n \
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {{\n \
f.debug_struct(\"{name}\").finish_non_exhaustive()\n \
}}\n\
}}"
);
}
self.zeroed_default(&name);
self.bitfield_accessors(&name, &layout);
let fields: Vec<&Field> = layout
.slots
.iter()
.filter_map(|s| match s {
Slot::Field(f) => Some(*f),
_ => None,
})
.collect();
self.layout_asserts(&name, body, &fields);
}
fn zeroed_default(&mut self, name: &str) {
let _ = writeln!(
self.out,
"\nimpl ::core::default::Default for {name} {{\n \
fn default() -> Self {{\n \
// SAFETY: generated records only contain integers, floats,\n \
// raw pointers, nullable function pointers and other such\n \
// records, for which all-zero bytes are a valid value.\n \
unsafe {{ ::core::mem::zeroed() }}\n \
}}\n\
}}"
);
}
fn layout_asserts(&mut self, name: &str, body: &RecordBody, fields: &[&Field]) {
let _ = writeln!(
self.out,
"\nconst _: () = {{\n \
assert!(::core::mem::size_of::<{name}>() == {});\n \
assert!(::core::mem::align_of::<{name}>() == {});",
body.size, body.align
);
for f in fields {
let _ = writeln!(
self.out,
" assert!(::core::mem::offset_of!({name}, {}) == {});",
ident(&f.name),
f.offset_bits / 8
);
}
self.out.push_str("};\n");
}
fn bitfield_accessors(&mut self, name: &str, layout: &Layout<'_>) {
let mut methods = String::new();
for slot in &layout.slots {
let Slot::Bitfield(g) = slot else { continue };
for f in &g.fields {
if f.name.starts_with("__anon") {
continue;
}
let width = f.bit_width.unwrap_or(0);
let bit = f.offset_bits - g.start * 8;
let ty = rust_type(&f.ty);
let storage = format!("_bitfield{}", g.index);
let (get, set) = self.bitfield_conversions(&f.ty, width);
let getter = ident(&f.name);
let setter = ident(&format!("set_{}", f.name));
self.doc_into(&mut methods, f.doc.as_deref());
let _ = writeln!(
methods,
" pub fn {getter}(&self) -> {ty} {{\n \
let raw = ::dlsym::__private::bitfield_get(&self.{storage}, {bit}, {width});\n \
{get}\n \
}}\n\n \
pub fn {setter}(&mut self, value: {ty}) {{\n \
::dlsym::__private::bitfield_set(&mut self.{storage}, {bit}, {width}, {set});\n \
}}\n"
);
}
}
if !methods.is_empty() {
let _ = write!(self.out, "\nimpl {name} {{\n{methods}}}\n");
}
}
fn doc_into(&self, out: &mut String, doc: Option<&str>) {
if let Some(doc) = doc {
for line in sanitize_doc(doc).lines() {
let _ = writeln!(out, " /// {line}");
}
}
}
fn bitfield_conversions(&self, ty: &Type, width: u32) -> (String, String) {
if let Type::Named(n) = ty
&& let Some(repr) = self.enums.get(n.as_str())
{
let n = ident(n);
let (get, _) = self.bitfield_conversions(repr, width);
return (format!("{n}({get})"), "value.0 as u64".to_owned());
}
let resolved = self.resolve(ty);
let ty_s = rust_type(ty);
if *resolved == Type::Bool {
return ("raw != 0".into(), "value as u64".into());
}
let signed = is_signed(resolved);
let get = if signed {
format!("((raw << (64 - {width})) as i64 >> (64 - {width})) as {ty_s}")
} else {
format!("raw as {ty_s}")
};
(get, "value as u64".into())
}
fn library(&mut self, items: &[&Item], opts: &Options) {
let mut blocks: Vec<(Abi, Vec<String>)> = Vec::new();
let mut names = HashSet::new();
for item in items {
let (abi, text) = match item {
Item::Function(f) => match self.function(f, opts) {
Ok(text) => (f.abi, text),
Err(why) => {
let _ = writeln!(self.out, "\n// skipped `{}`: {why}", f.name);
continue;
}
},
Item::Var(v) => (Abi::C, self.variable(v, opts)),
_ => continue,
};
names.insert(match item {
Item::Function(f) => f.name.as_str(),
Item::Var(v) => v.name.as_str(),
_ => unreachable!(),
});
match blocks.iter_mut().find(|(a, _)| *a == abi) {
Some((_, b)) => b.push(text),
None => blocks.push((abi, vec![text])),
}
}
let name = ident(&opts.struct_name);
let _ = writeln!(
self.out,
"\n::dlsym::library! {{\n \
/// Functions and variables, resolved when the library is loaded.\n \
pub struct {name} {{"
);
for (abi, items) in &blocks {
let _ = writeln!(self.out, " unsafe extern \"{}\" {{", abi.as_str());
for item in items {
self.out.push_str(item);
}
self.out.push_str(" }\n");
}
self.out.push_str(" }\n}\n");
if !opts.library_names.is_empty()
&& !names.contains("LIBRARY_NAMES")
&& !names.contains("open_default")
{
let list: Vec<String> = opts
.library_names
.iter()
.map(|n| format!("{n:?}"))
.collect();
let _ = writeln!(
self.out,
"\nimpl {name} {{\n \
/// Library file names tried, in order, by [`{name}::open_default`].\n \
pub const LIBRARY_NAMES: &'static [&'static str] = &[{}];\n\n \
/// Loads the first library in [`{name}::LIBRARY_NAMES`] that exists\n \
/// and resolves every symbol.\n \
///\n \
/// # Safety\n \
///\n \
/// Loading a library runs its initialisers; see\n \
/// [`dlsym::Library::open`].\n \
pub unsafe fn open_default() -> ::dlsym::Result<Self> {{\n \
// SAFETY: forwarded to the caller.\n \
unsafe {{ <Self as ::dlsym::Bindings>::open_first(Self::LIBRARY_NAMES) }}\n \
}}\n\
}}",
list.join(", ")
);
}
}
fn function(&self, f: &Function, opts: &Options) -> Result<String, String> {
if f.variadic {
if f.abi != Abi::C {
return Err("variadic functions are only supported with the C ABI".into());
}
if f.params.is_empty() || f.params.len() > 6 {
return Err("variadic functions need 1 to 6 fixed parameters".into());
}
}
let mut out = String::new();
if let Some(doc) = &f.doc {
let _ = writeln!(out, " #[doc = {:?}]", sanitize_doc(doc));
}
if opts.optional.iter().any(|p| glob(p, &f.name)) {
out.push_str(" #[optional]\n");
}
let fname = item_ident(&f.name, &mut out);
let safety = if !f.variadic && opts.safe.iter().any(|p| glob(p, &f.name)) {
"safe "
} else {
""
};
let mut params: Vec<String> = Vec::new();
let mut used = HashSet::new();
for (i, p) in f.params.iter().enumerate() {
let mut n = match &p.name {
Some(n) => ident(n),
None => format!("arg{i}"),
};
while !used.insert(n.clone()) {
n = format!("{n}_");
}
params.push(format!("{n}: {}", rust_type(&p.ty)));
}
if f.variadic {
params.push("...".into());
}
let ret = match &f.ret {
Type::Void => String::new(),
t => format!(" -> {}", rust_type(t)),
};
let _ = writeln!(
out,
" pub {safety}fn {fname}({}){ret};",
params.join(", ")
);
Ok(out)
}
fn variable(&self, v: &Var, opts: &Options) -> String {
let mut out = String::new();
if let Some(doc) = &v.doc {
let _ = writeln!(out, " #[doc = {:?}]", sanitize_doc(doc));
}
if opts.optional.iter().any(|p| glob(p, &v.name)) {
out.push_str(" #[optional]\n");
}
let name = item_ident(&v.name, &mut out);
let kind = match (v.is_const, opts.safe.iter().any(|p| glob(p, &v.name))) {
(false, _) => "static mut",
(true, true) => "safe static",
(true, false) => "static",
};
let _ = writeln!(out, " pub {kind} {name}: {};", rust_type(&v.ty));
out
}
}
fn item_ident(name: &str, out: &mut String) -> String {
let id = ident(name);
if id != name && !id.starts_with("r#") {
let _ = writeln!(out, " #[link_name = {name:?}]");
}
id
}
enum Slot<'a> {
Field(&'a Field),
Padding(usize, u64),
Bitfield(BitfieldGroup<'a>),
}
struct BitfieldGroup<'a> {
index: usize,
start: u64,
len: u64,
fields: Vec<&'a Field>,
}
struct Layout<'a> {
repr: String,
slots: Vec<Slot<'a>>,
}
fn struct_layout(body: &RecordBody) -> Option<Layout<'_>> {
enum Unit<'a> {
Field(&'a Field),
Bits(BitfieldGroup<'a>),
}
let mut units: Vec<Unit<'_>> = Vec::new();
let mut groups = 0;
for f in &body.fields {
match f.bit_width {
None => units.push(Unit::Field(f)),
Some(0) => {}
Some(w) => {
let end = (f.offset_bits + w as u64).div_ceil(8);
match units.last_mut() {
Some(Unit::Bits(g)) if f.offset_bits / 8 <= g.start + g.len => {
g.len = g.len.max(end - g.start);
g.fields.push(f);
}
_ => {
groups += 1;
let start = f.offset_bits / 8;
units.push(Unit::Bits(BitfieldGroup {
index: groups,
start,
len: end - start,
fields: vec![f],
}));
}
}
}
}
}
let place = |u: &Unit<'_>| -> (u64, u64, u64) {
match u {
Unit::Field(f) => (f.offset_bits / 8, f.layout.0, f.layout.1.max(1)),
Unit::Bits(g) => (g.start, g.len, 1),
}
};
if units
.iter()
.any(|u| matches!(u, Unit::Field(f) if f.offset_bits % 8 != 0))
{
return None;
}
let misaligned = units.iter().any(|u| {
let (off, _, align) = place(u);
off % align != 0
});
let pack = misaligned.then_some(body.align);
let effective = |align: u64| pack.map_or(align, |p| align.min(p));
let mut slots = Vec::new();
let mut cur = 0u64;
let mut max_align = 1u64;
let mut pads = 0;
for u in units {
let (off, size, align) = place(&u);
let align = effective(align);
max_align = max_align.max(align);
let natural = cur.next_multiple_of(align);
if off < natural {
return None;
}
if off > natural {
pads += 1;
slots.push(Slot::Padding(pads, off - cur));
}
cur = off + size;
slots.push(match u {
Unit::Field(f) => Slot::Field(f),
Unit::Bits(g) => Slot::Bitfield(g),
});
}
let repr = match pack {
Some(p) if max_align.min(p) != body.align && body.align > max_align => return None,
Some(1) => "C, packed".to_owned(),
Some(p) => format!("C, packed({p})"),
None if body.align > max_align => {
max_align = body.align;
format!("C, align({})", body.align)
}
None => "C".to_owned(),
};
let final_align = pack.map_or(max_align, |p| max_align.min(p));
if final_align != body.align {
return None;
}
if cur.next_multiple_of(final_align) < body.size {
pads += 1;
slots.push(Slot::Padding(pads, body.size - cur));
}
Some(Layout { repr, slots })
}
fn union_layout(body: &RecordBody) -> Option<Layout<'_>> {
if body
.fields
.iter()
.any(|f| f.bit_width.is_some() || f.offset_bits != 0)
{
return None;
}
let max_align = body
.fields
.iter()
.map(|f| f.layout.1.max(1))
.max()
.unwrap_or(1);
let max_size = body.fields.iter().map(|f| f.layout.0).max().unwrap_or(0);
let repr = if body.align > max_align {
format!("C, align({})", body.align)
} else if body.align < max_align {
if body.align == 1 {
"C, packed".to_owned()
} else {
format!("C, packed({})", body.align)
}
} else {
"C".to_owned()
};
let mut slots: Vec<Slot<'_>> = body.fields.iter().map(Slot::Field).collect();
if max_size.next_multiple_of(body.align) < body.size {
slots.push(Slot::Padding(1, body.size));
}
Some(Layout { repr, slots })
}
fn is_signed(t: &Type) -> bool {
matches!(
t,
Type::SChar
| Type::Short
| Type::Int
| Type::Long
| Type::LongLong
| Type::Fixed(_, true)
| Type::ISize
) || (*t == Type::Char && (c_char_is_signed()))
}
fn c_char_is_signed() -> bool {
std::ffi::c_char::MIN != 0
}
fn ident(name: &str) -> String {
match name {
"self" | "Self" | "super" | "crate" | "_" => format!("{name}_"),
n if KEYWORDS.contains(&n) => format!("r#{n}"),
n => n.to_owned(),
}
}
fn rust_type(ty: &Type) -> String {
let ffi = |name: &str| format!("::core::ffi::{name}");
match ty {
Type::Void => "()".into(),
Type::Bool => "bool".into(),
Type::Char => ffi("c_char"),
Type::SChar => ffi("c_schar"),
Type::UChar => ffi("c_uchar"),
Type::Short => ffi("c_short"),
Type::UShort => ffi("c_ushort"),
Type::Int => ffi("c_int"),
Type::UInt => ffi("c_uint"),
Type::Long => ffi("c_long"),
Type::ULong => ffi("c_ulong"),
Type::LongLong => ffi("c_longlong"),
Type::ULongLong => ffi("c_ulonglong"),
Type::Float => "f32".into(),
Type::Double => "f64".into(),
Type::Fixed(bits, true) => format!("i{bits}"),
Type::Fixed(bits, false) => format!("u{bits}"),
Type::USize => "usize".into(),
Type::ISize => "isize".into(),
Type::Pointer { pointee, is_const } => {
let qual = if *is_const { "const" } else { "mut" };
let inner = match **pointee {
Type::Void => ffi("c_void"),
ref t => rust_type(t),
};
format!("*{qual} {inner}")
}
Type::Array { elem, len } => format!("[{}; {len}]", rust_type(elem)),
Type::FnPtr {
params,
ret,
variadic,
abi,
} => {
let mut p: Vec<String> = params.iter().map(rust_type).collect();
if *variadic {
p.push("...".into());
}
let ret = match **ret {
Type::Void => String::new(),
ref t => format!(" -> {}", rust_type(t)),
};
format!(
"::core::option::Option<unsafe extern \"{}\" fn({}){ret}>",
abi.as_str(),
p.join(", ")
)
}
Type::Named(name) => ident(name),
Type::Unsupported(what) => format!("/* unsupported: {what} */ ()"),
}
}
fn float_literal(v: f64, ty: &str) -> String {
if v.is_nan() {
format!("{ty}::NAN")
} else if v.is_infinite() {
format!("{}{ty}::INFINITY", if v < 0.0 { "-" } else { "" })
} else {
let s = format!("{v:?}");
if s.contains(['.', 'e']) {
s
} else {
format!("{s}.0")
}
}
}
fn c_string_literal(bytes: &[u8]) -> String {
let mut s = String::from("c\"");
for &b in bytes {
match b {
b'"' => s.push_str("\\\""),
b'\\' => s.push_str("\\\\"),
0x20..=0x7e => s.push(b as char),
_ => {
let _ = write!(s, "\\x{b:02x}");
}
}
}
s.push('"');
s
}
fn sanitize_doc(doc: &str) -> String {
let risky = doc
.lines()
.any(|l| l.starts_with(" ") || l.starts_with('\t') || l.trim_start().starts_with("```"));
if risky {
format!("```text\n{}\n```", doc.replace("```", "'''"))
} else {
doc.to_owned()
}
}
fn glob(pattern: &str, name: &str) -> bool {
let (p, n) = (pattern.as_bytes(), name.as_bytes());
let (mut pi, mut ni) = (0, 0);
let (mut star, mut mark) = (None, 0);
while ni < n.len() {
if pi < p.len() && (p[pi] == b'?' || p[pi] == n[ni]) {
pi += 1;
ni += 1;
} else if pi < p.len() && p[pi] == b'*' {
star = Some(pi);
mark = ni;
pi += 1;
} else if let Some(s) = star {
pi = s + 1;
mark += 1;
ni = mark;
} else {
return false;
}
}
while pi < p.len() && p[pi] == b'*' {
pi += 1;
}
pi == p.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::ir::{Param, Variant};
fn field(name: &str, ty: Type, offset: u64, layout: (u64, u64)) -> Field {
Field {
name: name.into(),
ty,
offset_bits: offset * 8,
bit_width: None,
layout,
doc: None,
}
}
#[test]
fn idents() {
assert_eq!(ident("type"), "r#type");
assert_eq!(ident("self"), "self_");
assert_eq!(ident("deflate"), "deflate");
}
#[test]
fn natural_struct() {
let body = RecordBody {
size: 16,
align: 8,
fields: vec![
field("a", Type::Int, 0, (4, 4)),
field("b", Type::ptr(Type::Void), 8, (8, 8)),
],
};
let l = struct_layout(&body).unwrap();
assert_eq!(l.repr, "C");
assert_eq!(l.slots.len(), 2);
}
#[test]
fn packed_struct() {
let body = RecordBody {
size: 5,
align: 1,
fields: vec![
field("a", Type::UChar, 0, (1, 1)),
field("b", Type::Int, 1, (4, 4)),
],
};
assert_eq!(struct_layout(&body).unwrap().repr, "C, packed");
}
#[test]
fn over_aligned_struct() {
let body = RecordBody {
size: 64,
align: 64,
fields: vec![field("a", Type::Int, 0, (4, 4))],
};
let l = struct_layout(&body).unwrap();
assert_eq!(l.repr, "C, align(64)");
}
#[test]
fn bitfields_grouped() {
let mut a = field("a", Type::UInt, 0, (4, 4));
a.bit_width = Some(3);
let mut b = field("b", Type::UInt, 0, (4, 4));
b.offset_bits = 3;
b.bit_width = Some(5);
let body = RecordBody {
size: 4,
align: 4,
fields: vec![a, b],
};
let l = struct_layout(&body).unwrap();
assert_eq!(l.repr, "C, align(4)");
assert_eq!(l.slots.len(), 1);
assert!(matches!(&l.slots[0], Slot::Bitfield(g) if g.len == 1 && g.fields.len() == 2));
}
#[test]
fn emits_everything() {
let header = Header {
items: vec![
Item::Record(Record {
name: "opaque".into(),
kind: RecordKind::Struct,
body: None,
doc: None,
}),
Item::Enum(Enum {
name: Some("color".into()),
repr: Type::UInt,
variants: vec![Variant {
name: "RED".into(),
value: 1,
doc: None,
}],
doc: None,
}),
Item::Const(Const {
name: "VERSION".into(),
value: ConstValue::Str(b"1.0\n".to_vec()),
doc: None,
}),
Item::Function(Function {
name: "type".into(),
params: vec![Param {
name: None,
ty: Type::Named("color".into()),
}],
ret: Type::Void,
variadic: false,
abi: Abi::C,
doc: Some("Sets the type.".into()),
}),
Item::Var(Var {
name: "counter".into(),
ty: Type::Int,
is_const: false,
doc: None,
}),
],
};
let out = emit(
&header,
&Options {
struct_name: "Demo".into(),
library_names: vec!["libdemo.so".into()],
optional: vec!["count*".into()],
safe: vec!["type".into()],
..Default::default()
},
);
for needle in [
"pub struct opaque {",
"pub struct color(pub ::core::ffi::c_uint);",
"pub const RED: color = color(1);",
r#"pub const VERSION: &::core::ffi::CStr = c"1.0\x0a";"#,
"#[doc = \"Sets the type.\"]",
"pub safe fn r#type(arg0: color);",
"#[optional]\n pub static mut counter: ::core::ffi::c_int;",
"pub const LIBRARY_NAMES: &'static [&'static str] = &[\"libdemo.so\"];",
] {
assert!(out.contains(needle), "missing {needle:?} in:\n{out}");
}
}
#[test]
fn docs_with_code_are_fenced() {
assert_eq!(sanitize_doc("plain"), "plain");
assert_eq!(
sanitize_doc("ex:\n x = 1;"),
"```text\nex:\n x = 1;\n```"
);
}
}