use std::fmt::Write;
use super::{Emitter, Options, glob, ident, rust_type};
use crate::codegen::ir::{Function, Header, Item, Type};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Wrap {
pub pattern: String,
pub all_strings: bool,
pub strings: Vec<String>,
pub all_nullable_strings: bool,
pub nullable_strings: Vec<String>,
pub slices: Vec<(String, String)>,
pub outs: Vec<String>,
pub ret: Option<Ret>,
pub safe: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Ret {
NonZero,
Negative,
Errno,
StaticStr,
}
impl Wrap {
pub fn parse(spec: &str) -> Result<Self, String> {
let (pattern, annotations) = spec
.split_once(':')
.ok_or_else(|| format!("expected GLOB:ANNOTATION,..., got {spec:?}"))?;
let mut w = Wrap {
pattern: pattern.trim().to_owned(),
..Wrap::default()
};
for a in annotations
.split(',')
.map(str::trim)
.filter(|a| !a.is_empty())
{
let (key, value) = match a.split_once('=') {
Some((k, v)) => (k.trim(), Some(v.trim())),
None => (a, None),
};
match (key, value) {
("str", None) => w.all_strings = true,
("str", Some(name)) => w.strings.push(name.to_owned()),
("str?", None) => w.all_nullable_strings = true,
("str?", Some(name)) => w.nullable_strings.push(name.to_owned()),
("slice", Some(pair)) => {
let (ptr, len) = pair
.split_once('/')
.ok_or_else(|| format!("expected slice=PTR/LEN, got {a:?}"))?;
w.slices
.push((ptr.trim().to_owned(), len.trim().to_owned()));
}
("out", Some(name)) => w.outs.push(name.to_owned()),
("ret", Some(kind)) => {
w.ret = Some(match kind {
"nonzero" => Ret::NonZero,
"negative" => Ret::Negative,
"errno" => Ret::Errno,
"static-str" => Ret::StaticStr,
_ => return Err(format!("unknown ret= kind {kind:?} in {spec:?}")),
})
}
("safe", None) => w.safe = true,
_ => return Err(format!("unknown annotation {a:?} in {spec:?}")),
}
}
Ok(w)
}
pub(super) fn find(wraps: &[Wrap], name: &str) -> Option<Wrap> {
let mut found: Option<Wrap> = None;
for w in wraps.iter().filter(|w| glob(&w.pattern, name)) {
let m = found.get_or_insert_with(Wrap::default);
m.all_strings |= w.all_strings;
m.strings.extend(w.strings.iter().cloned());
m.all_nullable_strings |= w.all_nullable_strings;
m.nullable_strings
.extend(w.nullable_strings.iter().cloned());
m.slices.extend(w.slices.iter().cloned());
m.outs.extend(w.outs.iter().cloned());
m.ret = w.ret.or(m.ret);
m.safe |= w.safe;
}
found
}
}
pub(super) struct Plan {
pub params: String,
pub setup: String,
pub post: String,
pub args: Vec<String>,
pub ret_ty: String,
pub ret_expr: String,
pub uses_ret: bool,
}
impl Emitter<'_> {
pub(super) fn plan(
&self,
f: &Function,
skip: usize,
w: &Wrap,
lib: &str,
) -> Result<Plan, String> {
let params = &f.params[skip..];
let names: Vec<String> = params
.iter()
.enumerate()
.map(|(i, p)| p.name.clone().unwrap_or_else(|| format!("arg{}", i + skip)))
.collect();
let find = |n: &str| {
names
.iter()
.position(|m| m == n)
.ok_or_else(|| format!("no parameter `{n}`"))
};
enum Arg {
Plain,
Str,
OptStr,
Slice {
mutable: bool,
elem: String,
},
Len(usize),
Out(String),
OutOwned(String, String),
}
let mut how: Vec<Arg> = params.iter().map(|_| Arg::Plain).collect();
for (i, p) in params.iter().enumerate() {
let wanted = w.strings.contains(&names[i]);
let wanted_opt = w.nullable_strings.contains(&names[i]);
if (w.all_nullable_strings || wanted_opt) && self.is_c_string(&p.ty) {
how[i] = Arg::OptStr;
} else if (w.all_strings || wanted) && self.is_c_string(&p.ty) {
how[i] = Arg::Str;
} else if wanted || wanted_opt {
return Err(format!("`{}` is not a `const char *`", names[i]));
}
}
for (ptr, len) in &w.slices {
let (p, l) = (find(ptr)?, find(len)?);
let Type::Pointer { pointee, is_const } = self.resolve(¶ms[p].ty) else {
return Err(format!("`{ptr}` is not a pointer"));
};
if !is_integer(self.resolve(¶ms[l].ty)) {
return Err(format!("`{len}` is not an integer"));
}
let elem = match self.resolve(pointee) {
Type::Void | Type::Char => "u8".to_owned(),
_ => rust_type(pointee),
};
how[p] = Arg::Slice {
mutable: !is_const,
elem,
};
how[l] = Arg::Len(p);
}
for out in &w.outs {
let i = find(out)?;
let Type::Pointer {
pointee,
is_const: false,
} = self.resolve(¶ms[i].ty)
else {
return Err(format!("`{out}` is not a mutable pointer"));
};
if **pointee == Type::Void {
return Err(format!("`{out}` points to `void`"));
}
let owned = self
.owned_wrappers
.iter()
.find(|(t, _)| self.handle_param(pointee, t).is_some());
how[i] = match owned {
Some((_, wrapper)) => Arg::OutOwned(rust_type(pointee), wrapper.clone()),
None => Arg::Out(rust_type(pointee)),
};
}
let mut plan = Plan {
params: String::new(),
setup: String::new(),
post: String::new(),
args: Vec::new(),
ret_ty: String::new(),
ret_expr: String::new(),
uses_ret: true,
};
let mut outs: Vec<(String, String, bool)> = Vec::new();
for (i, a) in how.iter().enumerate() {
let n = ident(&names[i]);
match a {
Arg::Plain => {
let _ = write!(plan.params, ", {n}: {}", rust_type(¶ms[i].ty));
plan.args.push(n);
}
Arg::Str => {
let _ = write!(plan.params, ", {n}: &::core::ffi::CStr");
plan.args.push(format!("{n}.as_ptr()"));
}
Arg::OptStr => {
let _ = write!(
plan.params,
", {n}: ::core::option::Option<&::core::ffi::CStr>"
);
plan.args.push(format!(
"{n}.map_or(::core::ptr::null(), ::core::ffi::CStr::as_ptr)"
));
}
Arg::Slice { mutable, elem } => {
let (r, m) = if *mutable {
("&mut ", "as_mut_ptr")
} else {
("&", "as_ptr")
};
let _ = write!(plan.params, ", {n}: {r}[{elem}]");
plan.args.push(format!("{n}.{m}().cast()"));
}
Arg::Len(p) => {
let slice = ident(&names[*p]);
let ty = rust_type(¶ms[i].ty);
if ty == "usize" {
let _ = writeln!(plan.setup, " let {n} = {slice}.len();");
} else {
let _ = writeln!(
plan.setup,
" let {n} = <{ty}>::try_from({slice}.len()).expect(\"`{slice}` is too long\");"
);
}
plan.args.push(n);
}
Arg::Out(ty) => {
let _ = writeln!(
plan.setup,
" // SAFETY: generated types are valid when zeroed.\n \
let mut {n}: {ty} = unsafe {{ ::core::mem::zeroed() }};"
);
plan.args.push(format!("&raw mut {n}"));
outs.push((n, ty.clone(), false));
}
Arg::OutOwned(ty, wrapper) => {
let _ = writeln!(
plan.setup,
" // SAFETY: generated types are valid when zeroed.\n \
let mut {n}: {ty} = unsafe {{ ::core::mem::zeroed() }};"
);
plan.args.push(format!("&raw mut {n}"));
let _ = writeln!(
plan.post,
" // SAFETY: `{}` hands over the handle it stores in `{n}`.\n \
let {n} = unsafe {{ {wrapper}::from_raw({lib}, {n}) }};",
f.name
);
outs.push((n, format!("{wrapper}<'_>"), true));
}
}
}
let raw = (f.ret != Type::Void).then(|| rust_type(&f.ret));
let tuple = |with_ret: bool| {
let mut tys = Vec::new();
let mut exprs = Vec::new();
if with_ret && let Some(r) = &raw {
tys.push(r.clone());
exprs.push("ret".to_owned());
}
for (n, t, handle) in &outs {
if *handle && w.ret.is_none() {
tys.push(format!("::core::option::Option<{t}>"));
} else {
tys.push(t.clone());
}
exprs.push(n.clone());
}
match tys.len() {
1 => (tys.remove(0), exprs.remove(0)),
_ => (
format!("({})", tys.join(", ")),
format!("({})", exprs.join(", ")),
),
}
};
let code = || -> Result<String, String> {
match self.resolve(&f.ret) {
t if is_integer(t) => Ok("ret".to_owned()),
Type::Named(n) if self.enums.contains_key(n.as_str()) => Ok("ret.0".to_owned()),
_ => Err("ret= needs an integer return type".to_owned()),
}
};
let handles: Vec<&str> = outs.iter().filter(|o| o.2).map(|o| o.0.as_str()).collect();
let checked = |cond: String, ok: &str, null_err: &str, err: &str| {
if handles.is_empty() {
return format!(
"if {cond} {{ ::core::result::Result::Ok({ok}) }} else {{ {err} }}"
);
}
let somes: Vec<String> = handles
.iter()
.map(|h| format!("::core::option::Option::Some({h})"))
.collect();
format!(
"match ({cond}, {}) {{\n \
(true, {}) => ::core::result::Result::Ok({ok}),\n \
(true, ..) => {null_err},\n \
_ => {err},\n \
}}",
handles.join(", "),
somes.join(", ")
)
};
match w.ret {
None => {
let (ty, expr) = tuple(true);
plan.uses_ret = raw.is_some();
(plan.ret_ty, plan.ret_expr) = (ty, expr);
}
Some(Ret::NonZero) => {
let c = code()?;
let (ok, expr) = tuple(false);
let r = raw.clone().unwrap_or_default();
plan.ret_ty = format!("::core::result::Result<{ok}, {r}>");
let err = "::core::result::Result::Err(ret)";
plan.ret_expr = checked(format!("{c} == 0"), &expr, err, err);
}
Some(Ret::Negative) => {
let c = code()?;
let (ok, expr) = tuple(true);
let r = raw.clone().unwrap_or_default();
plan.ret_ty = format!("::core::result::Result<{ok}, {r}>");
let err = "::core::result::Result::Err(ret)";
plan.ret_expr = checked(format!("{c} >= 0"), &expr, err, err);
}
Some(Ret::Errno) => {
let c = code()?;
let (ok, expr) = tuple(true);
plan.ret_ty = format!("::std::io::Result<{ok}>");
plan.ret_expr = checked(
format!("{c} >= 0"),
&expr,
"::core::result::Result::Err(::std::io::Error::other(\"null handle\"))",
"::core::result::Result::Err(::dlsym::__private::errno_error())",
);
}
Some(Ret::StaticStr) => {
if !self.is_c_string(&f.ret) {
return Err("ret=static-str needs a `const char *` return type".into());
}
let s = "::core::option::Option<&::core::ffi::CStr>";
let conv = "if ret.is_null() { ::core::option::Option::None } else {\n \
// SAFETY: the library returns a NUL-terminated string that lives as\n \
// long as it does (`ret=static-str`), and `self` borrows the library.\n \
::core::option::Option::Some(unsafe { ::core::ffi::CStr::from_ptr(ret) })\n }";
if outs.is_empty() {
(plan.ret_ty, plan.ret_expr) = (s.into(), conv.into());
} else {
let (ty, expr) = tuple(false);
plan.ret_ty = format!("({s}, {ty})");
plan.ret_expr = format!("({conv}, {expr})");
}
}
}
Ok(plan)
}
pub(super) fn is_c_string(&self, ty: &Type) -> bool {
matches!(self.resolve(ty), Type::Pointer { pointee, is_const: true }
if matches!(self.resolve(pointee), Type::Char | Type::SChar | Type::UChar))
}
pub(super) fn wrapper_body(
plan: &Plan,
call: &str,
optional: bool,
unsafe_call: bool,
) -> String {
if plan.setup.is_empty()
&& plan.post.is_empty()
&& (plan.ret_expr == "ret" || !plan.uses_ret)
{
let call = if unsafe_call {
format!(
" // SAFETY: the requirements are the caller's (or declared met).\n \
unsafe {{ {call} }}\n"
)
} else {
format!(" {call}\n")
};
return call;
}
let q = if optional { "?" } else { "" };
let call = if unsafe_call {
format!("unsafe {{ {call} }}{q}")
} else {
format!("{call}{q}")
};
let mut body = plan.setup.clone();
if unsafe_call {
body.push_str(" // SAFETY: the arguments are converted from valid Rust values; the\n \
// remaining requirements are the caller's (or `safe` asserts them).\n");
}
let expr = if optional {
format!("::core::option::Option::Some({})", plan.ret_expr)
} else {
plan.ret_expr.clone()
};
if plan.uses_ret {
let _ = writeln!(body, " let ret = {call};");
} else {
let _ = writeln!(body, " {call};");
}
let _ = writeln!(body, "{} {expr}", plan.post);
body
}
pub(super) fn wrapper_ret(plan: &Plan, optional: bool) -> String {
match (plan.ret_ty.as_str(), optional) {
("()", false) | ("", false) => String::new(),
("()", true) | ("", true) => " -> ::core::option::Option<()>".into(),
(t, false) => format!(" -> {t}"),
(t, true) => format!(" -> ::core::option::Option<{t}>"),
}
}
pub(super) fn wrappers(&mut self, header: &Header, opts: &Options) {
if opts.wraps.is_empty() {
return;
}
let taken: std::collections::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 mut methods = String::new();
for item in &header.items {
let Item::Function(f) = item else { continue };
let Some(w) = Wrap::find(&opts.wraps, &f.name) else {
continue;
};
let name = format!("{}_rs", f.name);
if f.variadic || taken.contains(name.as_str()) {
let _ = writeln!(
self.out,
"\n// skipped wrapper `{name}`: variadic or name taken"
);
continue;
}
let plan = match self.plan(f, 0, &w, "self") {
Ok(p) => p,
Err(why) => {
let _ = writeln!(self.out, "\n// skipped wrapper `{name}`: {why}");
continue;
}
};
let optional = opts.optional.iter().any(|p| glob(p, &f.name));
let raw_safe = !f.variadic && opts.safe.iter().any(|p| glob(p, &f.name));
let call = format!("self.{}({})", ident(&f.name), plan.args.join(", "));
let body = Self::wrapper_body(&plan, &call, optional, !raw_safe);
let ret = Self::wrapper_ret(&plan, optional);
let (qual, safety) = if w.safe {
("", String::new())
} else {
(
"unsafe ",
format!(
" ///\n /// # Safety\n ///\n /// Same as calling `{}`, except for the converted arguments.\n",
f.name
),
)
};
let _ = write!(
methods,
" /// [`{0}`](Self::{0}) with Rust types.\n\
{safety} \
#[allow(clippy::too_many_arguments)]\n \
pub {qual}fn {1}(&self{2}){ret} {{\n{body} }}\n\n",
ident(&f.name),
ident(&name),
plan.params,
);
}
if !methods.is_empty() {
let _ = write!(
self.out,
"\nimpl {} {{\n{}}}\n",
ident(&opts.struct_name),
methods.trim_end_matches('\n').to_owned() + "\n"
);
}
}
}
fn is_integer(t: &Type) -> bool {
matches!(
t,
Type::Char
| Type::SChar
| Type::UChar
| Type::Short
| Type::UShort
| Type::Int
| Type::UInt
| Type::Long
| Type::ULong
| Type::LongLong
| Type::ULongLong
| Type::Fixed(..)
| Type::USize
| Type::ISize
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn specs() {
let w = Wrap::parse("gz*: str, slice=buf/len ,out=n,ret=errno,safe").unwrap();
assert_eq!(w.pattern, "gz*");
assert!(w.all_strings && w.safe);
assert_eq!(w.slices, [("buf".to_owned(), "len".to_owned())]);
assert_eq!(w.outs, ["n"]);
assert_eq!(w.ret, Some(Ret::Errno));
let w = Wrap::parse("f:str?,str?=name").unwrap();
assert!(w.all_nullable_strings && !w.all_strings);
assert_eq!(w.nullable_strings, ["name"]);
assert!(Wrap::parse("gzopen").is_err());
assert!(Wrap::parse("gzopen:slice=buf").is_err());
assert!(Wrap::parse("gzopen:ret=maybe").is_err());
assert!(Wrap::parse("gzopen:frobnicate").is_err());
let all = [
Wrap::parse("gz*:str").unwrap(),
Wrap::parse("gzread:slice=buf/len,ret=negative").unwrap(),
];
let m = Wrap::find(&all, "gzread").unwrap();
assert!(m.all_strings);
assert_eq!(m.ret, Some(Ret::Negative));
assert!(Wrap::find(&all, "deflate").is_none());
}
}