diff options
Diffstat (limited to '')
-rw-r--r-- | xpcom/idl-parser/xpidl/rust.py | 670 |
1 files changed, 670 insertions, 0 deletions
diff --git a/xpcom/idl-parser/xpidl/rust.py b/xpcom/idl-parser/xpidl/rust.py new file mode 100644 index 0000000000..bf4295d403 --- /dev/null +++ b/xpcom/idl-parser/xpidl/rust.py @@ -0,0 +1,670 @@ +# rust.py - Generate rust bindings from IDL. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +"""Print a runtime Rust bindings file for the IDL file specified""" + +# --- Safety Hazards --- + +# We currently don't generate some bindings for some IDL methods in rust code, +# due to there being ABI safety hazards if we were to do so. This is the +# documentation for the reasons why we don't generate certain types of bindings, +# so that we don't accidentally start generating them in the future. + +# notxpcom methods and attributes return their results directly by value. The x86 +# windows stdcall ABI returns aggregates by value differently for methods than +# functions, and rust only exposes the function ABI, so that's the one we're +# using. The correct ABI can be emulated for notxpcom methods returning aggregates +# by passing an &mut ReturnType parameter as the second parameter. This strategy +# is used by the winapi-rs crate. +# https://github.com/retep998/winapi-rs/blob/7338a5216a6a7abeefcc6bb1bc34381c81d3e247/src/macros.rs#L220-L231 +# +# Right now we can generate code for notxpcom methods and attributes, as we don't +# support passing aggregates by value over these APIs ever (the types which are +# allowed in xpidl.py shouldn't include any aggregates), so the code is +# correct. In the future if we want to start supporting returning aggregates by +# value, we will need to use a workaround such as the one used by winapi.rs. + +# nostdcall methods on x86 windows will use the thiscall ABI, which is not +# stable in rust right now, so we cannot generate bindings to them. + +# In general, passing C++ objects by value over the C ABI is not a good idea, +# and when possible we should avoid doing so. We don't generate bindings for +# these methods here currently. + +import os.path +import re + +from xpidl import xpidl + + +class AutoIndent(object): + """A small autoindenting wrapper around a fd. + Used to make the code output more readable.""" + + def __init__(self, fd): + self.fd = fd + self.indent = 0 + + def write(self, string): + """A smart write function which automatically adjusts the + indentation of each line as it is written by counting braces""" + for s in string.split("\n"): + s = s.strip() + indent = self.indent + if len(s) == 0: + indent = 0 + elif s[0] == "}": + indent -= 1 + + self.fd.write(" " * indent + s + "\n") + for c in s: + if c == "(" or c == "{" or c == "[": + self.indent += 1 + elif c == ")" or c == "}" or c == "]": + self.indent -= 1 + + +def rustSanitize(s): + keywords = [ + "abstract", + "alignof", + "as", + "become", + "box", + "break", + "const", + "continue", + "crate", + "do", + "else", + "enum", + "extern", + "false", + "final", + "fn", + "for", + "if", + "impl", + "in", + "let", + "loop", + "macro", + "match", + "mod", + "move", + "mut", + "offsetof", + "override", + "priv", + "proc", + "pub", + "pure", + "ref", + "return", + "Self", + "self", + "sizeof", + "static", + "struct", + "super", + "trait", + "true", + "type", + "typeof", + "unsafe", + "unsized", + "use", + "virtual", + "where", + "while", + "yield", + ] + if s in keywords: + return s + "_" + return s + + +# printdoccomments = False +printdoccomments = True + +if printdoccomments: + + def printComments(fd, clist, indent): + fd.write("%s%s" % (indent, doccomments(clist))) + + def doccomments(clist): + if len(clist) == 0: + return "" + s = "/// ```text" + for c in clist: + for cc in c.splitlines(): + s += "\n/// " + cc + s += "\n/// ```\n///\n" + return s + + +else: + + def printComments(fd, clist, indent): + pass + + def doccomments(clist): + return "" + + +def firstCap(str): + return str[0].upper() + str[1:] + + +# Attribute VTable Methods +def attributeNativeName(a, getter): + binaryname = rustSanitize(a.binaryname if a.binaryname else firstCap(a.name)) + return "%s%s" % ("Get" if getter else "Set", binaryname) + + +def attributeReturnType(a, getter): + if a.notxpcom: + if getter: + return a.realtype.rustType("in").strip() + return "::libc::c_void" + return "::nserror::nsresult" + + +def attributeParamName(a): + return "a" + firstCap(a.name) + + +def attributeRawParamList(iface, a, getter): + if getter and a.notxpcom: + l = [] + else: + l = [(attributeParamName(a), a.realtype.rustType("out" if getter else "in"))] + if a.implicit_jscontext: + raise xpidl.RustNoncompat("jscontext is unsupported") + if a.nostdcall: + raise xpidl.RustNoncompat("nostdcall is unsupported") + return l + + +def attributeParamList(iface, a, getter): + l = ["this: *const " + iface.name] + l += ["%s: %s" % x for x in attributeRawParamList(iface, a, getter)] + return ", ".join(l) + + +def attrAsVTableEntry(iface, m, getter): + try: + return 'pub %s: unsafe extern "system" fn (%s) -> %s' % ( + attributeNativeName(m, getter), + attributeParamList(iface, m, getter), + attributeReturnType(m, getter), + ) + except xpidl.RustNoncompat as reason: + return """\ +/// Unable to generate binding because `%s` +pub %s: *const ::libc::c_void""" % ( + reason, + attributeNativeName(m, getter), + ) + + +# Method VTable generation functions +def methodNativeName(m): + binaryname = m.binaryname is not None and m.binaryname or firstCap(m.name) + return rustSanitize(binaryname) + + +def methodReturnType(m): + if m.notxpcom: + return m.realtype.rustType("in").strip() + return "::nserror::nsresult" + + +def methodRawParamList(iface, m): + l = [(rustSanitize(p.name), p.rustType()) for p in m.params] + + if m.implicit_jscontext: + raise xpidl.RustNoncompat("jscontext is unsupported") + + if m.optional_argc: + raise xpidl.RustNoncompat("optional_argc is unsupported") + + if m.nostdcall: + raise xpidl.RustNoncompat("nostdcall is unsupported") + + if not m.notxpcom and m.realtype.name != "void": + l.append(("_retval", m.realtype.rustType("out"))) + + return l + + +def methodParamList(iface, m): + l = ["this: *const %s" % iface.name] + l += ["%s: %s" % x for x in methodRawParamList(iface, m)] + return ", ".join(l) + + +def methodAsVTableEntry(iface, m): + try: + return 'pub %s: unsafe extern "system" fn (%s) -> %s' % ( + methodNativeName(m), + methodParamList(iface, m), + methodReturnType(m), + ) + except xpidl.RustNoncompat as reason: + return """\ +/// Unable to generate binding because `%s` +pub %s: *const ::libc::c_void""" % ( + reason, + methodNativeName(m), + ) + + +method_impl_tmpl = """\ +#[inline] +pub unsafe fn %(name)s(&self, %(params)s) -> %(ret_ty)s { + ((*self.vtable).%(name)s)(self, %(args)s) +} +""" + + +def methodAsWrapper(iface, m): + try: + param_list = methodRawParamList(iface, m) + params = ["%s: %s" % x for x in param_list] + args = [x[0] for x in param_list] + + return method_impl_tmpl % { + "name": methodNativeName(m), + "params": ", ".join(params), + "ret_ty": methodReturnType(m), + "args": ", ".join(args), + } + except xpidl.RustNoncompat: + # Dummy field for the doc comments to attach to. + # Private so that it's not shown in rustdoc. + return "const _%s: () = ();" % methodNativeName(m) + + +infallible_impl_tmpl = """\ +#[inline] +pub unsafe fn %(name)s(&self) -> %(realtype)s { + let mut result = <%(realtype)s as ::std::default::Default>::default(); + let _rv = ((*self.vtable).%(name)s)(self, &mut result); + debug_assert!(_rv.succeeded()); + result +} +""" + + +def attrAsWrapper(iface, m, getter): + try: + if m.implicit_jscontext: + raise xpidl.RustNoncompat("jscontext is unsupported") + + if m.nostdcall: + raise xpidl.RustNoncompat("nostdcall is unsupported") + + name = attributeParamName(m) + + if getter and m.infallible and m.realtype.kind == "builtin": + # NOTE: We don't support non-builtin infallible getters in Rust code. + return infallible_impl_tmpl % { + "name": attributeNativeName(m, getter), + "realtype": m.realtype.rustType("in"), + } + + param_list = attributeRawParamList(iface, m, getter) + params = ["%s: %s" % x for x in param_list] + return method_impl_tmpl % { + "name": attributeNativeName(m, getter), + "params": ", ".join(params), + "ret_ty": attributeReturnType(m, getter), + "args": "" if getter and m.notxpcom else name, + } + + except xpidl.RustNoncompat: + # Dummy field for the doc comments to attach to. + # Private so that it's not shown in rustdoc. + return "const _%s: () = ();" % attributeNativeName(m, getter) + + +header = """\ +// +// DO NOT EDIT. THIS FILE IS GENERATED FROM $SRCDIR/%(relpath)s +// + +""" + + +def idl_basename(f): + """returns the base name of a file with the last extension stripped""" + return os.path.splitext(os.path.basename(f))[0] + + +def print_rust_bindings(idl, fd, relpath): + fd = AutoIndent(fd) + + fd.write(header % {"relpath": relpath}) + + # All of the idl files will be included into the same rust module, as we + # can't do forward declarations. Because of this, we want to ignore all + # import statements + + for p in idl.productions: + if p.kind == "include" or p.kind == "cdata" or p.kind == "forward": + continue + + if p.kind == "interface": + write_interface(p, fd) + continue + + if p.kind == "typedef": + try: + # We have to skip the typedef of bool to bool (it doesn't make any sense anyways) + if p.name == "bool": + continue + + if printdoccomments: + fd.write( + "/// `typedef %s %s;`\n///\n" + % (p.realtype.nativeType("in"), p.name) + ) + fd.write(doccomments(p.doccomments)) + fd.write("pub type %s = %s;\n\n" % (p.name, p.realtype.rustType("in"))) + except xpidl.RustNoncompat as reason: + fd.write( + "/* unable to generate %s typedef because `%s` */\n\n" + % (p.name, reason) + ) + + +base_vtable_tmpl = """ +/// We need to include the members from the base interface's vtable at the start +/// of the VTable definition. +pub __base: %sVTable, + +""" + + +vtable_tmpl = """\ +// This struct represents the interface's VTable. A pointer to a statically +// allocated version of this struct is at the beginning of every %(name)s +// object. It contains one pointer field for each method in the interface. In +// the case where we can't generate a binding for a method, we include a void +// pointer. +#[doc(hidden)] +#[repr(C)] +pub struct %(name)sVTable {%(base)s%(entries)s} + +""" + + +# NOTE: This template is not generated for nsISupports, as it has no base interfaces. +deref_tmpl = """\ +// Every interface struct type implements `Deref` to its base interface. This +// causes methods on the base interfaces to be directly avaliable on the +// object. For example, you can call `.AddRef` or `.QueryInterface` directly +// on any interface which inherits from `nsISupports`. +impl ::std::ops::Deref for %(name)s { + type Target = %(base)s; + #[inline] + fn deref(&self) -> &%(base)s { + unsafe { + ::std::mem::transmute(self) + } + } +} + +// Ensure we can use .coerce() to cast to our base types as well. Any type which +// our base interface can coerce from should be coercable from us as well. +impl<T: %(base)sCoerce> %(name)sCoerce for T { + #[inline] + fn coerce_from(v: &%(name)s) -> &Self { + T::coerce_from(v) + } +} +""" + + +struct_tmpl = """\ +// The actual type definition for the interface. This struct has methods +// declared on it which will call through its vtable. You never want to pass +// this type around by value, always pass it behind a reference. + +#[repr(C)] +pub struct %(name)s { + vtable: *const %(name)sVTable, + + /// This field is a phantomdata to ensure that the VTable type and any + /// struct containing it is not safe to send across threads, as XPCOM is + /// generally not threadsafe. + /// + /// XPCOM interfaces in general are not safe to send across threads. + __nosync: ::std::marker::PhantomData<::std::rc::Rc<u8>>, + + // Make the rust compiler aware that there might be interior mutability + // in what actually implements the interface. This works around UB + // introduced by + // https://github.com/llvm/llvm-project/commit/01859da84bad95fd51d6a03b08b60c660e642a4f + // that a rust lint would make blatantly obvious, but doesn't exist. + // (See https://github.com/rust-lang/rust/issues/111229). + // This prevents optimizations, but those optimizations weren't available + // before rustc switched to LLVM 16, and they now cause problems because + // of the UB. + // Until there's a lint available to find all our UB, it's simpler to + // avoid the UB in the first place, at the cost of preventing optimizations + // in places that don't cause UB. But again, those optimizations weren't + // available before. + __maybe_interior_mutability: ::std::cell::UnsafeCell<[u8; 0]>, +} + +// Implementing XpCom for an interface exposes its IID, which allows for easy +// use of the `.query_interface<T>` helper method. This also defines that +// method for %(name)s. +unsafe impl XpCom for %(name)s { + const IID: nsIID = nsID(0x%(m0)s, 0x%(m1)s, 0x%(m2)s, + [%(m3joined)s]); +} + +// We need to implement the RefCounted trait so we can be used with `RefPtr`. +// This trait teaches `RefPtr` how to manage our memory. +unsafe impl RefCounted for %(name)s { + #[inline] + unsafe fn addref(&self) { + self.AddRef(); + } + #[inline] + unsafe fn release(&self) { + self.Release(); + } +} + +// This trait is implemented on all types which can be coerced to from %(name)s. +// It is used in the implementation of `fn coerce<T>`. We hide it from the +// documentation, because it clutters it up a lot. +#[doc(hidden)] +pub trait %(name)sCoerce { + /// Cheaply cast a value of this type from a `%(name)s`. + fn coerce_from(v: &%(name)s) -> &Self; +} + +// The trivial implementation: We can obviously coerce ourselves to ourselves. +impl %(name)sCoerce for %(name)s { + #[inline] + fn coerce_from(v: &%(name)s) -> &Self { + v + } +} + +impl %(name)s { + /// Cast this `%(name)s` to one of its base interfaces. + #[inline] + pub fn coerce<T: %(name)sCoerce>(&self) -> &T { + T::coerce_from(self) + } +} +""" + + +wrapper_tmpl = """\ +// The implementations of the function wrappers which are exposed to rust code. +// Call these methods rather than manually calling through the VTable struct. +impl %(name)s { +%(consts)s +%(methods)s +} + +""" + +vtable_entry_tmpl = """\ +/* %(idl)s */ +%(entry)s, +""" + + +const_wrapper_tmpl = """\ +%(docs)s +pub const %(name)s: %(type)s = %(val)s; +""" + + +method_wrapper_tmpl = """\ +%(docs)s +/// `%(idl)s` +%(wrapper)s +""" + + +uuid_decoder = re.compile( + r"""(?P<m0>[a-f0-9]{8})- + (?P<m1>[a-f0-9]{4})- + (?P<m2>[a-f0-9]{4})- + (?P<m3>[a-f0-9]{4})- + (?P<m4>[a-f0-9]{12})$""", + re.X, +) + + +def write_interface(iface, fd): + if iface.namemap is None: + raise Exception("Interface was not resolved.") + + assert iface.base or (iface.name == "nsISupports") + + # Extract the UUID's information so that it can be written into the struct definition + names = uuid_decoder.match(iface.attributes.uuid).groupdict() + m3str = names["m3"] + names["m4"] + names["m3joined"] = ", ".join(["0x%s" % m3str[i : i + 2] for i in range(0, 16, 2)]) + names["name"] = iface.name + + if printdoccomments: + if iface.base is not None: + fd.write("/// `interface %s : %s`\n///\n" % (iface.name, iface.base)) + else: + fd.write("/// `interface %s`\n///\n" % iface.name) + printComments(fd, iface.doccomments, "") + fd.write(struct_tmpl % names) + + if iface.base is not None: + fd.write( + deref_tmpl + % { + "name": iface.name, + "base": iface.base, + } + ) + + entries = [] + for member in iface.members: + if type(member) == xpidl.Attribute: + entries.append( + vtable_entry_tmpl + % { + "idl": member.toIDL(), + "entry": attrAsVTableEntry(iface, member, True), + } + ) + if not member.readonly: + entries.append( + vtable_entry_tmpl + % { + "idl": member.toIDL(), + "entry": attrAsVTableEntry(iface, member, False), + } + ) + + elif type(member) == xpidl.Method: + entries.append( + vtable_entry_tmpl + % { + "idl": member.toIDL(), + "entry": methodAsVTableEntry(iface, member), + } + ) + + fd.write( + vtable_tmpl + % { + "name": iface.name, + "base": base_vtable_tmpl % iface.base if iface.base is not None else "", + "entries": "\n".join(entries), + } + ) + + # Get all of the constants + consts = [] + for member in iface.members: + if type(member) == xpidl.ConstMember: + consts.append( + const_wrapper_tmpl + % { + "docs": doccomments(member.doccomments), + "type": member.realtype.rustType("in"), + "name": member.name, + "val": member.getValue(), + } + ) + + methods = [] + for member in iface.members: + if type(member) == xpidl.Attribute: + methods.append( + method_wrapper_tmpl + % { + "docs": doccomments(member.doccomments), + "idl": member.toIDL(), + "wrapper": attrAsWrapper(iface, member, True), + } + ) + if not member.readonly: + methods.append( + method_wrapper_tmpl + % { + "docs": doccomments(member.doccomments), + "idl": member.toIDL(), + "wrapper": attrAsWrapper(iface, member, False), + } + ) + + elif type(member) == xpidl.Method: + methods.append( + method_wrapper_tmpl + % { + "docs": doccomments(member.doccomments), + "idl": member.toIDL(), + "wrapper": methodAsWrapper(iface, member), + } + ) + + fd.write( + wrapper_tmpl + % { + "name": iface.name, + "consts": "\n".join(consts), + "methods": "\n".join(methods), + } + ) |