use std::collections::{HashMap, HashSet};
use std::fmt::Write;
mod wrap;
pub use wrap::{Ret, Wrap};
use super::ir::{
Abi, Const, ConstValue, Enum, Field, Function, Header, Item, MacroArg, MacroFn, MemberKind,
Record, RecordBody, RecordKind, Type, Typedef, Var,
};
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Options {
pub struct_name: String,
pub library_names: Vec<String>,
pub optional: Vec<String>,
pub safe: Vec<String>,
pub safe_handle: Vec<String>,
pub comment: Vec<String>,
pub no_module_attributes: bool,
pub owned: Vec<Owned>,
pub wraps: Vec<Wrap>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Owned {
pub type_name: String,
pub free: String,
pub constructors: Vec<String>,
}
impl Owned {
pub fn new(
type_name: impl Into<String>,
free: impl Into<String>,
constructors: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Owned {
type_name: type_name.into(),
free: free.into(),
constructors: constructors.into_iter().map(Into::into).collect(),
}
}
pub fn parse(spec: &str) -> Result<Self, String> {
let (ty, rest) = spec
.split_once('=')
.ok_or_else(|| format!("expected TYPE=FREE[,CONSTRUCTOR...], got {spec:?}"))?;
let mut fns = rest.split(',').map(str::trim).filter(|s| !s.is_empty());
let free = fns
.next()
.ok_or_else(|| format!("missing free function in {spec:?}"))?;
Ok(Owned {
type_name: ty.trim().to_owned(),
free: free.to_owned(),
constructors: fns.map(str::to_owned).collect(),
})
}
}
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 BLOCK_SIZE: usize = 64;
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),
Item::MacroFn(_) => {}
}
}
e.library(&functions, opts);
e.owned_names(header, opts);
e.macro_fns(header, opts);
e.wrappers(header, opts);
e.owned(header, opts);
e.objects(header, opts);
e.out
}
struct Emitter<'h> {
out: String,
enums: HashMap<&'h str, &'h Type>,
typedefs: HashMap<&'h str, &'h Type>,
unions: HashSet<&'h str>,
wrappers: HashSet<String>,
owned_wrappers: HashMap<String, String>,
}
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(),
wrappers: HashSet::new(),
owned_wrappers: HashMap::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(), "");
if let (None, Some((size, align))) = (&r.body, r.opaque_layout) {
let _ = writeln!(
self.out,
"#[repr(C, align({align}))]\n\
#[derive(Debug)]\n\
pub struct {name} {{\n \
_opaque: [u8; {size}],\n \
_marker: ::core::marker::PhantomData<(*mut u8, ::core::marker::PhantomPinned)>,\n\
}}\n\n\
const _: () = {{\n \
assert!(::core::mem::size_of::<{name}>() == {size});\n \
assert!(::core::mem::align_of::<{name}>() == {align});\n\
}};"
);
return;
}
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,
" {}{}: {},",
if f.is_private { "" } else { "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") || f.is_private {
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()
.rev()
.find(|(a, b)| *a == abi && b.len() < BLOCK_SIZE)
{
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 owned_names(&mut self, header: &Header, opts: &Options) {
let mut taken: HashSet<String> = header
.items
.iter()
.filter_map(|i| i.type_name().map(str::to_owned))
.collect();
taken.insert(opts.struct_name.clone());
for o in &opts.owned {
let valid = header.items.iter().any(|i| {
matches!(i, Item::Function(f) if f.name == o.free
&& matches!(f.params.as_slice(), [p] if self.handle_param(&p.ty, &o.type_name).is_some()))
});
if !valid || self.owned_wrappers.contains_key(&o.type_name) {
continue;
}
let mut wrapper = camel_case(&o.type_name);
while !taken.insert(wrapper.clone()) {
wrapper.push_str("Owned");
}
self.wrappers.insert(wrapper.clone());
self.owned_wrappers.insert(o.type_name.clone(), wrapper);
}
}
fn owned(&mut self, header: &Header, opts: &Options) {
let functions: HashMap<&str, &Function> = header
.items
.iter()
.filter_map(|i| match i {
Item::Function(f) => Some((f.name.as_str(), f)),
_ => None,
})
.collect();
let method_names: HashSet<&str> = header
.items
.iter()
.filter_map(|i| match i {
Item::Function(f) => Some(f.name.as_str()),
Item::Var(v) => Some(v.name.as_str()),
Item::MacroFn(m) => Some(m.name.as_str()),
_ => None,
})
.collect();
let lib = ident(&opts.struct_name);
let mut ctor_methods = String::new();
for o in &opts.owned {
let Some(free) = functions.get(o.free.as_str()) else {
let _ = writeln!(
self.out,
"\n// skipped owned `{}`: no function `{}`",
o.type_name, o.free
);
continue;
};
let [param] = free.params.as_slice() else {
let _ = writeln!(
self.out,
"\n// skipped owned `{}`: `{}` must take one argument",
o.type_name, o.free
);
continue;
};
if self.handle_param(¶m.ty, &o.type_name).is_none() {
let _ = writeln!(
self.out,
"\n// skipped owned `{}`: `{}` does not take a `{0}` handle",
o.type_name, o.free
);
continue;
}
let raw = rust_type(¶m.ty);
let Some(wrapper) = self.owned_wrappers.get(&o.type_name).cloned() else {
continue;
};
let free_call = format!("self.lib.{}(self.raw)", ident(&free.name));
let _ = writeln!(
self.out,
"\n/// An owned `{ty}` handle, released with `{free}` when dropped.\n\
///\n\
/// It borrows the [`{lib}`] it came from, so it cannot outlive the library.\n\
pub struct {wrapper}<'lib> {{\n \
raw: {raw},\n \
lib: &'lib {lib},\n\
}}\n\n\
impl<'lib> {wrapper}<'lib> {{\n \
/// Takes ownership of `raw`. Returns `None` if it is null.\n \
///\n \
/// # Safety\n \
///\n \
/// `raw` must be a live handle from `lib` that nothing else releases.\n \
pub unsafe fn from_raw(lib: &'lib {lib}, raw: {raw}) -> ::core::option::Option<Self> {{\n \
if (raw as *const ::core::ffi::c_void).is_null() {{\n \
::core::option::Option::None\n \
}} else {{\n \
::core::option::Option::Some(Self {{ raw, lib }})\n \
}}\n \
}}\n\n \
/// Returns the handle, still owned by `self`.\n \
pub fn as_raw(&self) -> {raw} {{\n \
self.raw\n \
}}\n\n \
/// Gives up ownership: the caller must release the handle.\n \
pub fn into_raw(self) -> {raw} {{\n \
let raw = self.raw;\n \
::core::mem::forget(self);\n \
raw\n \
}}\n\n \
/// Returns the bindings the handle came from.\n \
pub fn library(&self) -> &'lib {lib} {{\n \
self.lib\n \
}}\n\
}}\n\n\
impl ::core::ops::Drop for {wrapper}<'_> {{\n \
fn drop(&mut self) {{\n \
// SAFETY: `self` owns the handle, so it is released exactly once.\n \
let _ = unsafe {{ {free_call} }};\n \
}}\n\
}}\n\n\
impl ::core::fmt::Debug for {wrapper}<'_> {{\n \
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {{\n \
f.debug_tuple(\"{wrapper}\").field(&(self.raw as *const ::core::ffi::c_void)).finish()\n \
}}\n\
}}",
ty = o.type_name,
free = o.free,
);
self.handle_methods(header, o, &wrapper, opts);
for ctor in &o.constructors {
let Some(f) = functions.get(ctor.as_str()) else {
let _ = writeln!(
self.out,
"// skipped constructor `{ctor}`: no such function"
);
continue;
};
let method = format!("{ctor}_owned");
if f.variadic || rust_type(&f.ret) != raw || method_names.contains(method.as_str())
{
let _ = writeln!(
self.out,
"// skipped constructor `{ctor}`: does not return a `{}`",
o.type_name
);
continue;
}
let w = ctor_wrap(&opts.wraps, ctor);
let plan = match self.plan(f, 0, &w, "self") {
Ok(p) => p,
Err(why) => {
let _ = writeln!(self.out, "// `{ctor}`: annotations ignored: {why}");
match self.plan(f, 0, &Wrap::default(), "self") {
Ok(p) => p,
Err(_) => continue,
}
}
};
let optional = opts.optional.iter().any(|p| glob(p, ctor));
let safe = w.safe || opts.safe.iter().any(|p| glob(p, ctor));
let call = format!(
"self.{}({}){}",
ident(ctor),
plan.args.join(", "),
if optional { "?" } else { "" }
);
let setup = &plan.setup;
let (qual, safety_doc) = if safe {
("", String::new())
} else {
(
"unsafe ",
format!(
" ///\n /// # Safety\n ///\n /// Same as calling `{ctor}`.\n"
),
)
};
let _ = write!(
ctor_methods,
" /// Calls `{ctor}` and takes ownership of the result (`None` if it is null).\n\
{safety_doc} \
#[allow(clippy::too_many_arguments)]\n \
pub {qual}fn {method}(&self{}) -> ::core::option::Option<{wrapper}<'_>> {{\n\
{setup} \
// SAFETY: `{ctor}` returns a new handle owned by the caller.\n \
unsafe {{ {wrapper}::from_raw(self, {call}) }}\n \
}}\n\n",
plan.params
);
}
}
if !ctor_methods.is_empty() {
let _ = write!(
self.out,
"\nimpl {lib} {{\n{}}}\n",
ctor_methods.trim_end_matches('\n').to_owned() + "\n"
);
}
}
fn objects(&mut self, header: &Header, opts: &Options) {
let lib = ident(&opts.struct_name);
let mut types: HashSet<String> = header
.items
.iter()
.filter_map(|i| i.type_name().map(str::to_owned))
.collect();
types.extend(self.wrappers.iter().cloned());
types.insert(opts.struct_name.clone());
for item in &header.items {
let Item::Record(r) = item else { continue };
if r.opaque_layout.is_none() && r.body.is_none()
|| opts.owned.iter().any(|o| o.type_name == r.name)
{
continue;
}
let members = |kind| {
header.items.iter().filter_map(move |i| match i {
Item::Function(f)
if f.member
.as_ref()
.is_some_and(|m| m.class == r.name && m.kind == kind)
&& !opts.optional.iter().any(|p| glob(p, &f.name)) =>
{
Some(f)
}
_ => None,
})
};
let Some(dtor) = members(MemberKind::Destructor).next() else {
continue;
};
let ctors: Vec<&Function> = members(MemberKind::Constructor).collect();
if ctors.is_empty() {
continue;
}
let ty = ident(&r.name);
let mut wrapper = camel_case(&r.name);
while !types.insert(wrapper.clone()) {
wrapper.push_str("Object");
}
self.wrappers.insert(wrapper.clone());
let mut ctor_fns = String::new();
let prefix = format!("{}_", r.name);
for f in &ctors {
let method = f.name.strip_prefix(&prefix).unwrap_or(&f.name);
let w = ctor_wrap(&opts.wraps, &f.name);
let plan = match self.plan(f, 1, &w, "lib") {
Ok(p) => p,
Err(why) => {
let _ =
writeln!(ctor_fns, " // `{}`: annotations ignored: {why}", f.name);
match self.plan(f, 1, &Wrap::default(), "lib") {
Ok(p) => p,
Err(_) => continue,
}
}
};
let (params, setup) = (&plan.params, &plan.setup);
let args: String = plan.args.iter().map(|a| format!(", {a}")).collect();
let safe = w.safe
|| opts
.safe
.iter()
.chain(&opts.safe_handle)
.any(|p| glob(p, &f.name));
let (qual, safety_doc) = if safe {
("", "")
} else {
(
"unsafe ",
" ///\n /// # Safety\n ///\n /// Same as calling the constructor.\n",
)
};
let _ = writeln!(
ctor_fns,
" /// Allocates and constructs an object with [`{0}`]({lib}::{0}).\n\
{safety_doc} \
#[allow(clippy::too_many_arguments)]\n \
pub {qual}fn {1}(lib: &'lib {lib}{params}) -> Self {{\n\
{setup} \
let storage = ::dlsym::__private::ObjectStorage::<{ty}>::new();\n \
// SAFETY: `storage` is uninitialized memory for one object; if the\n \
// constructor unwinds, dropping `storage` frees it.\n \
unsafe {{ lib.{0}(storage.as_ptr(){args}) }};\n \
Self {{\n \
raw: storage.into_raw(),\n \
lib,\n \
}}\n \
}}\n",
ident(&f.name),
ident(method),
);
}
let _ = writeln!(
self.out,
"\n/// An owned [`{ty}`] object: allocated by Rust, constructed in place, and\n\
/// destroyed with [`{dtor}`]({lib}::{dtor}) and freed when dropped.\n\
///\n\
/// The object never moves (C++ objects may point to themselves), and it\n\
/// borrows the [`{lib}`] it came from, so it cannot outlive the library.\n\
pub struct {wrapper}<'lib> {{\n \
raw: *mut {ty},\n \
lib: &'lib {lib},\n\
}}\n\n\
impl<'lib> {wrapper}<'lib> {{\n\
{ctor_fns} \
/// The object's address, valid while `self` lives.\n \
pub fn as_ptr(&self) -> *const {ty} {{\n \
self.raw\n \
}}\n\n \
/// The object's address, valid while `self` lives.\n \
pub fn as_mut_ptr(&mut self) -> *mut {ty} {{\n \
self.raw\n \
}}\n\n \
/// Returns the bindings the object came from.\n \
pub fn library(&self) -> &'lib {lib} {{\n \
self.lib\n \
}}\n\
}}\n\n\
impl ::core::ops::Drop for {wrapper}<'_> {{\n \
fn drop(&mut self) {{\n \
// SAFETY: the object is live and destroyed exactly once.\n \
unsafe {{ self.lib.{dtor}(self.raw) }};\n \
// SAFETY: the storage came from `ObjectStorage::into_raw`.\n \
unsafe {{ ::dlsym::__private::ObjectStorage::free(self.raw) }};\n \
}}\n\
}}\n\n\
impl ::core::fmt::Debug for {wrapper}<'_> {{\n \
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {{\n \
f.debug_tuple(\"{wrapper}\").field(&self.raw).finish()\n \
}}\n\
}}",
dtor = ident(&dtor.name),
);
let handle = Owned {
type_name: r.name.clone(),
free: dtor.name.clone(),
constructors: ctors.iter().map(|f| f.name.clone()).collect(),
};
self.handle_methods(header, &handle, &wrapper, opts);
}
}
fn handle_param(&self, ty: &Type, type_name: &str) -> Option<bool> {
match ty {
Type::Pointer { pointee, is_const } if **pointee == Type::Named(type_name.into()) => {
Some(*is_const)
}
Type::Named(n)
if n == type_name && matches!(self.resolve(ty), Type::Pointer { .. }) =>
{
Some(false)
}
_ => None,
}
}
fn handle_methods(&mut self, header: &Header, o: &Owned, wrapper: &str, opts: &Options) {
const RESERVED: &[&str] = &[
"from_raw",
"as_raw",
"into_raw",
"as_ptr",
"as_mut_ptr",
"new",
"library",
"drop",
"fmt",
];
let candidates: Vec<(&Function, bool)> = header
.items
.iter()
.filter_map(|i| match i {
Item::Function(f)
if !f.variadic && f.name != o.free && !o.constructors.contains(&f.name) =>
{
let first = f.params.first()?;
Some((f, self.handle_param(&first.ty, &o.type_name)?))
}
_ => None,
})
.collect();
let prefix = format!("{}_", o.type_name);
let short: Vec<Option<&str>> = candidates
.iter()
.map(|(f, _)| {
f.name
.strip_prefix(&prefix)
.filter(|s| !s.is_empty() && !s.starts_with(|c: char| c.is_ascii_digit()))
})
.collect();
let mut used: HashSet<&str> = RESERVED.iter().copied().collect();
let mut methods = String::new();
for (i, (f, is_const)) in candidates.iter().enumerate() {
let name = match short[i] {
Some(s)
if !used.contains(s)
&& !candidates.iter().any(|(g, _)| g.name == s)
&& short.iter().filter(|t| **t == Some(s)).count() == 1 =>
{
s
}
_ => f.name.as_str(),
};
if !used.insert(name) {
continue;
}
let w = Wrap::find(&opts.wraps, &f.name).unwrap_or_default();
let plan = match self.plan(f, 1, &w, "self.lib") {
Ok(p) => p,
Err(why) => {
let _ = writeln!(methods, " // `{name}`: annotations ignored: {why}");
match self.plan(f, 1, &Wrap::default(), "self.lib") {
Ok(p) => p,
Err(_) => continue,
}
}
};
let optional = opts.optional.iter().any(|p| glob(p, &f.name));
let ret = Self::wrapper_ret(&plan, optional);
let call = format!(
"self.lib.{}(self.raw{})",
ident(&f.name),
plan.args
.iter()
.map(|a| format!(", {a}"))
.collect::<String>()
);
let params = &plan.params;
let receiver = if *is_const { "&self" } else { "&mut self" };
let _ = writeln!(
methods,
" /// Calls [`{0}`]({1}::{0}) on this handle.",
ident(&f.name),
ident(&opts.struct_name)
);
let raw_safe = opts.safe.iter().any(|p| glob(p, &f.name));
if raw_safe || w.safe || opts.safe_handle.iter().any(|p| glob(p, &f.name)) {
let body = Self::wrapper_body(&plan, &call, optional, !raw_safe);
let _ = writeln!(
methods,
" #[allow(clippy::too_many_arguments)]\n \
pub fn {}({receiver}{params}){ret} {{\n{body} }}\n",
ident(name)
);
} else {
let body = Self::wrapper_body(&plan, &call, optional, true);
let _ = writeln!(
methods,
" ///\n /// # Safety\n ///\n \
/// Same as calling `{0}` with the handle, which must not release it.\n \
#[allow(clippy::too_many_arguments)]\n \
pub unsafe fn {1}({receiver}{params}){ret} {{\n{body} }}\n",
f.name,
ident(name)
);
}
}
if !methods.is_empty() {
let _ = write!(
self.out,
"\nimpl {wrapper}<'_> {{\n{}}}\n",
methods.trim_end_matches('\n').to_owned() + "\n"
);
}
}
fn macro_fns(&mut self, header: &Header, opts: &Options) {
let mut functions = HashMap::new();
let mut consts = HashMap::new();
let mut types = HashSet::new();
for item in &header.items {
match item {
Item::Function(f) => {
functions.insert(f.name.as_str(), f);
}
Item::Const(c) => {
consts.insert(c.name.as_str(), &c.value);
}
Item::Typedef(t) => {
types.insert(t.name.as_str());
}
Item::Record(r) => {
types.insert(r.name.as_str());
}
Item::Enum(Enum { name: Some(n), .. }) => {
types.insert(n.as_str());
}
_ => {}
}
}
let mut methods = String::new();
for item in &header.items {
let Item::MacroFn(m) = item else { continue };
match self.macro_fn(m, &functions, &consts, &types, opts) {
Some(text) => methods.push_str(&text),
None => {
let _ = writeln!(
self.out,
"\n// skipped macro `{}`: arguments not representable",
m.name
);
}
}
}
if !methods.is_empty() {
let _ = write!(
self.out,
"\nimpl {} {{\n{methods}}}\n",
ident(&opts.struct_name)
);
}
}
fn macro_fn(
&self,
m: &MacroFn,
functions: &HashMap<&str, &Function>,
consts: &HashMap<&str, &ConstValue>,
types: &HashSet<&str>,
opts: &Options,
) -> Option<String> {
let f = functions.get(m.target.as_str())?;
if f.variadic || f.params.len() != m.args.len() {
return None;
}
let mut param_types = vec![None; m.params.len()];
let mut args = Vec::new();
for (arg, p) in m.args.iter().zip(&f.params) {
let ty = rust_type(&p.ty);
let is_ptr = matches!(p.ty, Type::Pointer { .. });
args.push(match arg {
MacroArg::Param(i) => {
param_types[*i] = Some(ty);
ident(&m.params[*i])
}
MacroArg::Int(v) => format!("{v} as {ty}"),
MacroArg::Str(bytes) if is_ptr => format!("{}.as_ptr()", c_string_literal(bytes)),
MacroArg::Const(name) => match consts.get(name.as_str())? {
ConstValue::Str(_) if is_ptr => format!("{}.as_ptr()", ident(name)),
ConstValue::Int(..) | ConstValue::Float(..) if !is_ptr => {
format!("{} as {ty}", ident(name))
}
_ => return None,
},
MacroArg::SizeOf(t) if types.contains(t.as_str()) && !is_ptr => {
format!("::core::mem::size_of::<{}>() as {ty}", ident(t))
}
_ => return None,
});
}
let params: Vec<String> = m
.params
.iter()
.zip(param_types)
.map(|(n, t)| Some(format!("{}: {}", ident(n), t?)))
.collect::<Option<_>>()?;
let optional = opts.optional.iter().any(|p| glob(p, &f.name));
let safe = opts.safe.iter().any(|p| glob(p, &f.name));
let ret = match (&f.ret, optional) {
(Type::Void, false) => String::new(),
(Type::Void, true) => " -> ::core::option::Option<()>".into(),
(t, false) => format!(" -> {}", rust_type(t)),
(t, true) => format!(" -> ::core::option::Option<{}>", rust_type(t)),
};
let target = ident(&f.name);
let call = format!("self.{target}({})", args.join(", "));
let mut out = format!(
" /// Wraps the C macro `{}`, which calls [`{target}`](Self::{target}).\n",
m.name
);
if safe {
let _ = writeln!(
out,
" #[allow(clippy::too_many_arguments)]\n \
pub fn {}(&self, {}){ret} {{\n {call}\n }}\n",
ident(&m.name),
params.join(", ")
);
} else {
let _ = writeln!(
out,
" ///\n /// # Safety\n ///\n /// Same as calling `{target}`.\n \
#[allow(clippy::too_many_arguments)]\n \
pub unsafe fn {}(&self, {}){ret} {{\n \
// SAFETY: forwarded to the caller.\n \
unsafe {{ {call} }}\n }}\n",
ident(&m.name),
params.join(", ")
);
}
Some(out)
}
fn function(&self, f: &Function, opts: &Options) -> Result<String, String> {
if f.variadic {
if !matches!(f.abi, Abi::C | Abi::CUnwind) {
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, f.symbol(), &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, v.symbol(), &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 ctor_wrap(wraps: &[Wrap], name: &str) -> Wrap {
let mut w = Wrap::find(wraps, name).unwrap_or_default();
w.ret = None;
w.outs.clear();
w
}
fn item_ident(name: &str, symbol: &str, out: &mut String) -> String {
let id = ident(name);
if symbol != id.strip_prefix("r#").unwrap_or(&id) {
let _ = writeln!(out, " #[link_name = {symbol:?}]");
}
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 camel_case(name: &str) -> String {
let mut out = String::new();
for part in name.split('_').filter(|p| !p.is_empty()) {
let mut chars = part.chars();
if let Some(first) = chars.next() {
out.push(first.to_ascii_uppercase());
out.extend(chars);
}
}
if out.is_empty() || out.starts_with(|c: char| c.is_ascii_digit()) {
out.insert(0, 'H');
}
out
}
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,
is_private: false,
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,
opaque_layout: 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(),
link_name: None,
params: vec![Param {
name: None,
ty: Type::Named("color".into()),
}],
ret: Type::Void,
variadic: false,
abi: Abi::C,
doc: Some("Sets the type.".into()),
member: None,
}),
Item::Var(Var {
name: "counter".into(),
link_name: None,
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 owned_specs() {
let o = Owned::parse("gzFile=gzclose,gzopen, gzdopen").unwrap();
assert_eq!(o.type_name, "gzFile");
assert_eq!(o.free, "gzclose");
assert_eq!(o.constructors, ["gzopen", "gzdopen"]);
assert!(Owned::parse("gzFile").is_err());
assert!(Owned::parse("gzFile=").is_err());
assert_eq!(camel_case("demo_handle"), "DemoHandle");
assert_eq!(camel_case("gzFile"), "GzFile");
}
#[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```"
);
}
}