diff options
Diffstat (limited to 'xpcom/idl-parser')
-rw-r--r-- | xpcom/idl-parser/setup.py | 18 | ||||
-rw-r--r-- | xpcom/idl-parser/xpidl/__init__.py | 0 | ||||
-rw-r--r-- | xpcom/idl-parser/xpidl/header.py | 723 | ||||
-rw-r--r-- | xpcom/idl-parser/xpidl/jsonxpt.py | 282 | ||||
-rw-r--r-- | xpcom/idl-parser/xpidl/moz.build | 11 | ||||
-rw-r--r-- | xpcom/idl-parser/xpidl/python.toml | 4 | ||||
-rwxr-xr-x | xpcom/idl-parser/xpidl/runtests.py | 257 | ||||
-rw-r--r-- | xpcom/idl-parser/xpidl/rust.py | 693 | ||||
-rw-r--r-- | xpcom/idl-parser/xpidl/rust_macros.py | 111 | ||||
-rwxr-xr-x | xpcom/idl-parser/xpidl/xpidl.py | 2124 |
10 files changed, 4223 insertions, 0 deletions
diff --git a/xpcom/idl-parser/setup.py b/xpcom/idl-parser/setup.py new file mode 100644 index 0000000000..af6b417cb7 --- /dev/null +++ b/xpcom/idl-parser/setup.py @@ -0,0 +1,18 @@ +# 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/. + +from setuptools import find_packages, setup + +setup( + name="xpidl", + version="1.0", + description="Parser and header generator for xpidl files.", + author="Mozilla Foundation", + license="MPL 2.0", + packages=find_packages(), + install_requires=["ply>=3.6,<4.0"], + url="https://github.com/pelmers/xpidl", + entry_points={"console_scripts": ["header.py = xpidl.header:main"]}, + keywords=["xpidl", "parser"], +) diff --git a/xpcom/idl-parser/xpidl/__init__.py b/xpcom/idl-parser/xpidl/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/xpcom/idl-parser/xpidl/__init__.py diff --git a/xpcom/idl-parser/xpidl/header.py b/xpcom/idl-parser/xpidl/header.py new file mode 100644 index 0000000000..ed179b1bad --- /dev/null +++ b/xpcom/idl-parser/xpidl/header.py @@ -0,0 +1,723 @@ +#!/usr/bin/env python +# header.py - Generate C++ header files 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 C++ header file for the IDL files specified on the command line""" + +import itertools +import os.path +import re + +from xpidl import xpidl + +printdoccomments = False + +if printdoccomments: + + def printComments(fd, clist, indent): + for c in clist: + fd.write("%s%s\n" % (indent, c)) + +else: + + def printComments(fd, clist, indent): + pass + + +def firstCap(str): + return str[0].upper() + str[1:] + + +def attributeParamName(a): + return "a" + firstCap(a.name) + + +def attributeParamNames(a, getter, return_param=True): + if getter and (a.notxpcom or not return_param): + l = [] + else: + l = [attributeParamName(a)] + if a.implicit_jscontext: + l.insert(0, "cx") + return ", ".join(l) + + +def attributeNativeName(a, getter): + binaryname = a.binaryname is not None and a.binaryname or firstCap(a.name) + return "%s%s" % (getter and "Get" or "Set", binaryname) + + +def attributeAttributes(a, getter): + ret = "" + + if a.must_use: + ret = "[[nodiscard]] " + ret + + # Ideally, we'd set MOZ_CAN_RUN_SCRIPT in the "scriptable and not + # builtinclass" case too, so we'd just have memberCanRunScript() check + # explicit_setter_can_run_script/explicit_setter_can_run_script and call it + # here. But that would likely require a fair amount of Gecko-side + # annotation work. See bug 1534292. + if (a.explicit_getter_can_run_script and getter) or ( + a.explicit_setter_can_run_script and not getter + ): + ret = "MOZ_CAN_RUN_SCRIPT " + ret + + return ret + + +def attributeReturnType(a, getter, macro): + """macro should be NS_IMETHOD or NS_IMETHODIMP""" + # Pick the type to be returned from the getter/setter. + if a.notxpcom: + ret = a.realtype.nativeType("in").strip() if getter else "void" + else: + ret = "nsresult" + + # Set calling convention and virtual-ness + if a.nostdcall: + if macro == "NS_IMETHOD": + # This is the declaration. + ret = "virtual %s" % ret + else: + if ret == "nsresult": + ret = macro + else: + ret = "%s_(%s)" % (macro, ret) + + return attributeAttributes(a, getter) + ret + + +def attributeParamlist(a, getter, return_param=True): + if getter and (a.notxpcom or not return_param): + l = [] + else: + l = [ + "%s%s" + % (a.realtype.nativeType(getter and "out" or "in"), attributeParamName(a)) + ] + if a.implicit_jscontext: + l.insert(0, "JSContext* cx") + + return ", ".join(l) + + +def attributeAsNative(a, getter, declType="NS_IMETHOD"): + params = { + "returntype": attributeReturnType(a, getter, declType), + "binaryname": attributeNativeName(a, getter), + "paramlist": attributeParamlist(a, getter), + } + return "%(returntype)s %(binaryname)s(%(paramlist)s)" % params + + +def methodNativeName(m): + return m.binaryname is not None and m.binaryname or firstCap(m.name) + + +def methodAttributes(m): + ret = "" + + if m.must_use: + ret = "[[nodiscard]] " + ret + + # Ideally, we'd set MOZ_CAN_RUN_SCRIPT in the "scriptable and not + # builtinclass" case too, so we'd just have memberCanRunScript() check + # explicit_can_run_script and call it here. But that would likely require + # a fair amount of Gecko-side annotation work. See bug 1534292. + if m.explicit_can_run_script: + ret = "MOZ_CAN_RUN_SCRIPT " + ret + + return ret + + +def methodReturnType(m, macro): + """macro should be NS_IMETHOD or NS_IMETHODIMP""" + if m.notxpcom: + ret = m.realtype.nativeType("in").strip() + else: + ret = "nsresult" + + # Set calling convention and virtual-ness + if m.nostdcall: + if macro == "NS_IMETHOD": + # This is the declaration + ret = "virtual %s" % ret + else: + if ret == "nsresult": + ret = macro + else: + ret = "%s_(%s)" % (macro, ret) + + return methodAttributes(m) + ret + + +def methodAsNative(m, declType="NS_IMETHOD"): + return "%s %s(%s)" % ( + methodReturnType(m, declType), + methodNativeName(m), + paramlistAsNative(m), + ) + + +def paramlistAsNative(m, empty="void", return_param=True): + l = [paramAsNative(p) for p in m.params] + + if m.implicit_jscontext: + l.append("JSContext* cx") + + if m.optional_argc: + l.append("uint8_t _argc") + + if not m.notxpcom and m.realtype.name != "void" and return_param: + l.append( + paramAsNative( + xpidl.Param( + paramtype="out", + type=None, + name="_retval", + attlist=[], + location=None, + realtype=m.realtype, + ) + ) + ) + + # Set any optional out params to default to nullptr. Skip if we just added + # extra non-optional args to l. + if len(l) == len(m.params): + paramIter = len(m.params) - 1 + while ( + paramIter >= 0 + and m.params[paramIter].optional + and "out" in m.params[paramIter].paramtype + ): + t = m.params[paramIter].type + # Strings can't be optional, so this shouldn't happen, but let's make sure: + if t == "AString" or t == "ACString" or t == "AUTF8String": + break + l[paramIter] += " = nullptr" + paramIter -= 1 + + if len(l) == 0: + return empty + + return ", ".join(l) + + +def memberCanRunScript(member): + # This can only happen if the member is scriptable and its interface is not builtinclass. + return member.isScriptable() and not member.iface.attributes.builtinclass + + +def runScriptAnnotation(member): + return "JS_HAZ_CAN_RUN_SCRIPT " if memberCanRunScript(member) else "" + + +def paramAsNative(p): + default_spec = "" + if p.default_value: + default_spec = " = " + p.default_value + return "%s%s%s" % (p.nativeType(), p.name, default_spec) + + +def paramlistNames(m, return_param=True): + names = [p.name for p in m.params] + + if m.implicit_jscontext: + names.append("cx") + + if m.optional_argc: + names.append("_argc") + + if not m.notxpcom and m.realtype.name != "void" and return_param: + names.append("_retval") + + if len(names) == 0: + return "" + return ", ".join(names) + + +header = """/* + * DO NOT EDIT. THIS FILE IS GENERATED FROM $SRCDIR/%(relpath)s + */ + +#ifndef __gen_%(basename)s_h__ +#define __gen_%(basename)s_h__ +""" + +include = """ +#include "%(basename)s.h" +""" + +jsvalue_include = """ +#include "js/Value.h" +""" + +infallible_includes = """ +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" +""" + +can_run_script_includes = """ +#include "js/GCAnnotations.h" +""" + +header_end = """/* For IDL files that don't want to include root IDL files. */ +#ifndef NS_NO_VTABLE +#define NS_NO_VTABLE +#endif +""" + +footer = """ +#endif /* __gen_%(basename)s_h__ */ +""" + +forward_decl = """class %(name)s; /* forward declaration */ + +""" + + +def idl_basename(f): + """returns the base name of a file with the last extension stripped""" + return os.path.basename(f).rpartition(".")[0] + + +def print_header(idl, fd, filename, relpath): + fd.write(header % {"relpath": relpath, "basename": idl_basename(filename)}) + + foundinc = False + for inc in idl.includes(): + if not foundinc: + foundinc = True + fd.write("\n") + fd.write(include % {"basename": idl_basename(inc.filename)}) + + if idl.needsJSTypes(): + fd.write(jsvalue_include) + + # Include some extra files if any attributes are infallible. + interfaces = [p for p in idl.productions if p.kind == "interface"] + wroteRunScriptIncludes = False + wroteInfallibleIncludes = False + for iface in interfaces: + for member in iface.members: + if not isinstance(member, xpidl.Attribute) and not isinstance( + member, xpidl.Method + ): + continue + if not wroteInfallibleIncludes and member.infallible: + fd.write(infallible_includes) + wroteInfallibleIncludes = True + if not wroteRunScriptIncludes and memberCanRunScript(member): + fd.write(can_run_script_includes) + wroteRunScriptIncludes = True + if wroteRunScriptIncludes and wroteInfallibleIncludes: + break + + fd.write("\n") + fd.write(header_end) + + for p in idl.productions: + if p.kind == "include": + continue + if p.kind == "cdata": + fd.write(p.data) + continue + + if p.kind == "webidl": + write_webidl(p, fd) + continue + if p.kind == "forward": + fd.write(forward_decl % {"name": p.name}) + continue + if p.kind == "interface": + write_interface(p, fd) + continue + if p.kind == "typedef": + printComments(fd, p.doccomments, "") + fd.write("typedef %s %s;\n\n" % (p.realtype.nativeType("in"), p.name)) + + fd.write(footer % {"basename": idl_basename(filename)}) + + +def write_webidl(p, fd): + path = p.native.split("::") + for seg in path[:-1]: + fd.write("namespace %s {\n" % seg) + fd.write("class %s; /* webidl %s */\n" % (path[-1], p.name)) + for seg in reversed(path[:-1]): + fd.write("} // namespace %s\n" % seg) + fd.write("\n") + + +iface_header = r""" +/* starting interface: %(name)s */ +#define %(defname)s_IID_STR "%(iid)s" + +#define %(defname)s_IID \ + {0x%(m0)s, 0x%(m1)s, 0x%(m2)s, \ + { %(m3joined)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, +) + +iface_prolog = """ { + public: + + NS_DECLARE_STATIC_IID_ACCESSOR(%(defname)s_IID) + +""" + +iface_scriptable = """\ + /* Used by ToJSValue to check which scriptable interface is implemented. */ + using ScriptableInterfaceType = %(name)s; + +""" + +iface_epilog = """}; + + NS_DEFINE_STATIC_IID_ACCESSOR(%(name)s, %(defname)s_IID)""" + +iface_decl = """ + +/* Use this macro when declaring classes that implement this interface. */ +#define NS_DECL_%(macroname)s """ + +iface_nonvirtual = """ + +/* Use this macro when declaring the members of this interface when the + class doesn't implement the interface. This is useful for forwarding. */ +#define NS_DECL_NON_VIRTUAL_%(macroname)s """ + +iface_forward = """ + +/* Use this macro to declare functions that forward the behavior of this interface to another object. */ +#define NS_FORWARD_%(macroname)s(_to) """ # NOQA: E501 + +iface_forward_safe = """ + +/* Use this macro to declare functions that forward the behavior of this interface to another object in a safe way. */ +#define NS_FORWARD_SAFE_%(macroname)s(_to) """ # NOQA: E501 + +builtin_infallible_tmpl = """\ + %(attributes)sinline %(realtype)s %(nativename)s(%(args)s) + { + %(realtype)sresult; + mozilla::DebugOnly<nsresult> rv = %(nativename)s(%(argnames)s&result); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return result; + } +""" + +# NOTE: We don't use RefPtr::forget here because we don't want to need the +# definition of %(realtype)s in scope, which we would need for the +# AddRef/Release calls. +refcnt_infallible_tmpl = """\ + %(attributes)s inline already_AddRefed<%(realtype)s> %(nativename)s(%(args)s) + { + %(realtype)s* result = nullptr; + mozilla::DebugOnly<nsresult> rv = %(nativename)s(%(argnames)s&result); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return already_AddRefed<%(realtype)s>(result); + } +""" + +iface_threadsafe_tmpl = """\ +namespace mozilla::detail { +template <> +class InterfaceNeedsThreadSafeRefCnt<%(name)s> : public std::true_type {}; +} +""" + + +def infallibleDecl(member): + isattr = isinstance(member, xpidl.Attribute) + ismethod = isinstance(member, xpidl.Method) + assert isattr or ismethod + + realtype = member.realtype.nativeType("in") + tmpl = builtin_infallible_tmpl + + if member.realtype.kind != "builtin" and member.realtype.kind != "cenum": + assert realtype.endswith(" *"), "bad infallible type" + tmpl = refcnt_infallible_tmpl + realtype = realtype[:-2] # strip trailing pointer + + if isattr: + nativename = attributeNativeName(member, getter=True) + args = attributeParamlist(member, getter=True, return_param=False) + argnames = attributeParamNames(member, getter=True, return_param=False) + attributes = attributeAttributes(member, getter=True) + else: + nativename = methodNativeName(member) + args = paramlistAsNative(member, return_param=False) + argnames = paramlistNames(member, return_param=False) + attributes = methodAttributes(member) + + return tmpl % { + "attributes": attributes, + "realtype": realtype, + "nativename": nativename, + "args": args, + "argnames": argnames + ", " if argnames else "", + } + + +def write_interface(iface, fd): + if iface.namemap is None: + raise Exception("Interface was not resolved.") + + # Confirm that no names of methods will overload in this interface + names = set() + + def record_name(name): + if name in names: + raise Exception( + "Unexpected overloaded virtual method %s in interface %s" + % (name, iface.name) + ) + names.add(name) + + for m in iface.members: + if type(m) == xpidl.Attribute: + record_name(attributeNativeName(m, getter=True)) + if not m.readonly: + record_name(attributeNativeName(m, getter=False)) + elif type(m) == xpidl.Method: + record_name(methodNativeName(m)) + + def write_const_decls(g): + fd.write(" enum {\n") + enums = [] + for c in g: + printComments(fd, c.doccomments, " ") + basetype = c.basetype + value = c.getValue() + enums.append( + " %(name)s = %(value)s%(signed)s" + % { + "name": c.name, + "value": value, + "signed": (not basetype.signed) and "U" or "", + } + ) + fd.write(",\n".join(enums)) + fd.write("\n };\n\n") + + def write_cenum_decl(b): + fd.write(" enum %s : uint%d_t {\n" % (b.basename, b.width)) + for var in b.variants: + fd.write(" %s = %s,\n" % (var.name, var.value)) + fd.write(" };\n\n") + + def write_method_decl(m): + printComments(fd, m.doccomments, " ") + + fd.write(" /* %s */\n" % m.toIDL()) + fd.write(" %s%s = 0;\n\n" % (runScriptAnnotation(m), methodAsNative(m))) + + if m.infallible: + fd.write(infallibleDecl(m)) + + def write_attr_decl(a): + printComments(fd, a.doccomments, " ") + + fd.write(" /* %s */\n" % a.toIDL()) + + fd.write(" %s%s = 0;\n" % (runScriptAnnotation(a), attributeAsNative(a, True))) + if a.infallible: + fd.write(infallibleDecl(a)) + + if not a.readonly: + fd.write( + " %s%s = 0;\n" % (runScriptAnnotation(a), attributeAsNative(a, False)) + ) + fd.write("\n") + + defname = iface.name.upper() + if iface.name[0:2] == "ns": + defname = "NS_" + defname[2:] + + 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)]) + + if iface.name[2] == "I": + implclass = iface.name[:2] + iface.name[3:] + else: + implclass = "_MYCLASS_" + + names.update( + { + "defname": defname, + "macroname": iface.name.upper(), + "name": iface.name, + "iid": iface.attributes.uuid, + "implclass": implclass, + } + ) + + fd.write(iface_header % names) + + printComments(fd, iface.doccomments, "") + + fd.write("class ") + foundcdata = False + for m in iface.members: + if isinstance(m, xpidl.CDATA): + foundcdata = True + + if not foundcdata: + fd.write("NS_NO_VTABLE ") + + fd.write(iface.name) + if iface.base: + fd.write(" : public %s" % iface.base) + fd.write(iface_prolog % names) + + if iface.attributes.scriptable: + fd.write(iface_scriptable % names) + + for key, group in itertools.groupby(iface.members, key=type): + if key == xpidl.ConstMember: + write_const_decls(group) # iterator of all the consts + else: + for member in group: + if key == xpidl.Attribute: + write_attr_decl(member) + elif key == xpidl.Method: + write_method_decl(member) + elif key == xpidl.CDATA: + fd.write(" %s" % member.data) + elif key == xpidl.CEnum: + write_cenum_decl(member) + else: + raise Exception("Unexpected interface member: %s" % member) + + fd.write(iface_epilog % names) + + if iface.attributes.rust_sync: + fd.write(iface_threadsafe_tmpl % names) + + fd.write(iface_decl % names) + + def writeDeclaration(fd, iface, virtual): + declType = "NS_IMETHOD" if virtual else "nsresult" + suffix = " override" if virtual else "" + for member in iface.members: + if isinstance(member, xpidl.Attribute): + if member.infallible: + fd.write( + "\\\n using %s::%s; " + % (iface.name, attributeNativeName(member, True)) + ) + fd.write( + "\\\n %s%s; " % (attributeAsNative(member, True, declType), suffix) + ) + if not member.readonly: + fd.write( + "\\\n %s%s; " + % (attributeAsNative(member, False, declType), suffix) + ) + elif isinstance(member, xpidl.Method): + fd.write("\\\n %s%s; " % (methodAsNative(member, declType), suffix)) + if len(iface.members) == 0: + fd.write("\\\n /* no methods! */") + elif member.kind not in ("attribute", "method"): + fd.write("\\") + + writeDeclaration(fd, iface, True) + fd.write(iface_nonvirtual % names) + writeDeclaration(fd, iface, False) + fd.write(iface_forward % names) + + def emitTemplate(forward_infallible, tmpl, tmpl_notxpcom=None): + if tmpl_notxpcom is None: + tmpl_notxpcom = tmpl + for member in iface.members: + if isinstance(member, xpidl.Attribute): + if forward_infallible and member.infallible: + fd.write( + "\\\n using %s::%s; " + % (iface.name, attributeNativeName(member, True)) + ) + attr_tmpl = tmpl_notxpcom if member.notxpcom else tmpl + fd.write( + attr_tmpl + % { + "asNative": attributeAsNative(member, True), + "nativeName": attributeNativeName(member, True), + "paramList": attributeParamNames(member, True), + } + ) + if not member.readonly: + fd.write( + attr_tmpl + % { + "asNative": attributeAsNative(member, False), + "nativeName": attributeNativeName(member, False), + "paramList": attributeParamNames(member, False), + } + ) + elif isinstance(member, xpidl.Method): + if member.notxpcom: + fd.write( + tmpl_notxpcom + % { + "asNative": methodAsNative(member), + "nativeName": methodNativeName(member), + "paramList": paramlistNames(member), + } + ) + else: + fd.write( + tmpl + % { + "asNative": methodAsNative(member), + "nativeName": methodNativeName(member), + "paramList": paramlistNames(member), + } + ) + if len(iface.members) == 0: + fd.write("\\\n /* no methods! */") + elif member.kind not in ("attribute", "method"): + fd.write("\\") + + emitTemplate( + True, + "\\\n %(asNative)s override { return _to %(nativeName)s(%(paramList)s); } ", + ) + + fd.write(iface_forward_safe % names) + + # Don't try to safely forward notxpcom functions, because we have no + # sensible default error return. Instead, the caller will have to + # implement them. + emitTemplate( + False, + "\\\n %(asNative)s override { return !_to ? NS_ERROR_NULL_POINTER : _to->%(nativeName)s(%(paramList)s); } ", # NOQA: E501 + "\\\n %(asNative)s override; ", + ) + + fd.write("\n\n") + + +def main(outputfile): + xpidl.IDLParser() + + +if __name__ == "__main__": + main(None) diff --git a/xpcom/idl-parser/xpidl/jsonxpt.py b/xpcom/idl-parser/xpidl/jsonxpt.py new file mode 100644 index 0000000000..342188f645 --- /dev/null +++ b/xpcom/idl-parser/xpidl/jsonxpt.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python +# jsonxpt.py - Generate json XPT typelib files 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/. + +"""Generate a json XPT typelib for an IDL file""" + +import itertools +import json + +from xpidl import xpidl + +# A map of xpidl.py types to xpt enum variants +TypeMap = { + # builtins + "boolean": "TD_BOOL", + "void": "TD_VOID", + "int16_t": "TD_INT16", + "int32_t": "TD_INT32", + "int64_t": "TD_INT64", + "uint8_t": "TD_UINT8", + "uint16_t": "TD_UINT16", + "uint32_t": "TD_UINT32", + "uint64_t": "TD_UINT64", + "octet": "TD_UINT8", + "short": "TD_INT16", + "long": "TD_INT32", + "long long": "TD_INT64", + "unsigned short": "TD_UINT16", + "unsigned long": "TD_UINT32", + "unsigned long long": "TD_UINT64", + "float": "TD_FLOAT", + "double": "TD_DOUBLE", + "char": "TD_CHAR", + "string": "TD_PSTRING", + "wchar": "TD_WCHAR", + "wstring": "TD_PWSTRING", + # special types + "nsid": "TD_NSID", + "astring": "TD_ASTRING", + "utf8string": "TD_UTF8STRING", + "cstring": "TD_CSTRING", + "jsval": "TD_JSVAL", + "promise": "TD_PROMISE", +} + + +def flags(*flags): + return [flag for flag, cond in flags if cond] + + +def get_type(type, calltype, iid_is=None, size_is=None): + while isinstance(type, xpidl.Typedef): + type = type.realtype + + if isinstance(type, xpidl.Builtin): + ret = {"tag": TypeMap[type.name]} + if type.name in ["string", "wstring"] and size_is is not None: + ret["tag"] += "_SIZE_IS" + ret["size_is"] = size_is + return ret + + if isinstance(type, xpidl.Array): + # NB: For a Array<T> we pass down the iid_is to get the type of T. + # This allows Arrays of InterfaceIs types to work. + return { + "tag": "TD_ARRAY", + "element": get_type(type.type, calltype, iid_is), + } + + if isinstance(type, xpidl.LegacyArray): + # NB: For a Legacy [array] T we pass down iid_is to get the type of T. + # This allows [array] of InterfaceIs types to work. + return { + "tag": "TD_LEGACY_ARRAY", + "size_is": size_is, + "element": get_type(type.type, calltype, iid_is), + } + + if isinstance(type, xpidl.Interface) or isinstance(type, xpidl.Forward): + return { + "tag": "TD_INTERFACE_TYPE", + "name": type.name, + } + + if isinstance(type, xpidl.WebIDL): + return { + "tag": "TD_DOMOBJECT", + "name": type.name, + "native": type.native, + "headerFile": type.headerFile, + } + + if isinstance(type, xpidl.Native): + if type.specialtype == "nsid" and type.isPtr(calltype): + return {"tag": "TD_NSIDPTR"} + elif type.specialtype: + return {"tag": TypeMap[type.specialtype]} + elif iid_is is not None: + return { + "tag": "TD_INTERFACE_IS_TYPE", + "iid_is": iid_is, + } + else: + return {"tag": "TD_VOID"} + + if isinstance(type, xpidl.CEnum): + # As far as XPConnect is concerned, cenums are just unsigned integers. + return {"tag": "TD_UINT%d" % type.width} + + raise Exception("Unknown type!") + + +def mk_param(type, in_=0, out=0, optional=0): + return { + "type": type, + "flags": flags( + ("in", in_), + ("out", out), + ("optional", optional), + ), + } + + +def mk_method(method, params, getter=0, setter=0, optargc=0, hasretval=0, symbol=0): + return { + "name": method.name, + # NOTE: We don't include any return value information here, as we'll + # never call the methods if they're marked notxpcom, and all xpcom + # methods return the same type (nsresult). + # XXX: If we ever use these files for other purposes than xptcodegen we + # may want to write that info. + "params": params, + "flags": flags( + ("getter", getter), + ("setter", setter), + ("hidden", method.noscript or method.notxpcom), + ("optargc", optargc), + ("jscontext", method.implicit_jscontext), + ("hasretval", hasretval), + ("symbol", method.symbol), + ), + } + + +def attr_param_idx(p, m, attr): + attr_val = getattr(p, attr, None) + if not attr_val: + return None + for i, param in enumerate(m.params): + if param.name == attr_val: + return i + raise Exception(f"Need parameter named '{attr_val}' for attribute '{attr}'") + + +def build_interface(iface): + if iface.namemap is None: + raise Exception("Interface was not resolved.") + + assert ( + iface.attributes.scriptable + ), "Don't generate XPT info for non-scriptable interfaces" + + # State used while building an interface + consts = [] + methods = [] + + def build_const(c): + consts.append( + { + "name": c.name, + "type": get_type(c.basetype, ""), + "value": c.getValue(), # All of our consts are numbers + } + ) + + def build_cenum(b): + for var in b.variants: + consts.append( + { + "name": var.name, + "type": get_type(b, "in"), + "value": var.value, + } + ) + + def build_method(m): + params = [] + for p in m.params: + params.append( + mk_param( + get_type( + p.realtype, + p.paramtype, + iid_is=attr_param_idx(p, m, "iid_is"), + size_is=attr_param_idx(p, m, "size_is"), + ), + in_=p.paramtype.count("in"), + out=p.paramtype.count("out"), + optional=p.optional, + ) + ) + + hasretval = len(m.params) > 0 and m.params[-1].retval + if not m.notxpcom and m.realtype.name != "void": + hasretval = True + params.append(mk_param(get_type(m.realtype, "out"), out=1)) + + methods.append( + mk_method(m, params, optargc=m.optional_argc, hasretval=hasretval) + ) + + def build_attr(a): + assert a.realtype.name != "void" + # Write the getter + getter_params = [] + if not a.notxpcom: + getter_params.append(mk_param(get_type(a.realtype, "out"), out=1)) + + methods.append(mk_method(a, getter_params, getter=1, hasretval=1)) + + # And maybe the setter + if not a.readonly: + param = mk_param(get_type(a.realtype, "in"), in_=1) + methods.append(mk_method(a, [param], setter=1)) + + for member in iface.members: + if isinstance(member, xpidl.ConstMember): + build_const(member) + elif isinstance(member, xpidl.Attribute): + build_attr(member) + elif isinstance(member, xpidl.Method): + build_method(member) + elif isinstance(member, xpidl.CEnum): + build_cenum(member) + elif isinstance(member, xpidl.CDATA): + pass + else: + raise Exception("Unexpected interface member: %s" % member) + + return { + "name": iface.name, + "uuid": iface.attributes.uuid, + "methods": methods, + "consts": consts, + "parent": iface.base, + "flags": flags( + ("function", iface.attributes.function), + ("builtinclass", iface.attributes.builtinclass), + ("main_process_only", iface.attributes.main_process_scriptable_only), + ), + } + + +# These functions are the public interface of this module. They are very simple +# functions, but are exported so that if we need to do something more +# complex in them in the future we can. + + +def build_typelib(idl): + """Given a parsed IDL file, generate and return the typelib""" + return [ + build_interface(p) + for p in idl.productions + if p.kind == "interface" and p.attributes.scriptable + ] + + +def link(typelibs): + """Link a list of typelibs together into a single typelib""" + linked = list(itertools.chain.from_iterable(typelibs)) + assert len(set(iface["name"] for iface in linked)) == len( + linked + ), "Multiple typelibs containing the same interface were linked together" + return linked + + +def write(typelib, fd): + """Write typelib into fd""" + json.dump(typelib, fd, indent=2, sort_keys=True) diff --git a/xpcom/idl-parser/xpidl/moz.build b/xpcom/idl-parser/xpidl/moz.build new file mode 100644 index 0000000000..60252a0b81 --- /dev/null +++ b/xpcom/idl-parser/xpidl/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +PYTHON_UNITTEST_MANIFESTS += [ + "python.toml", +] + +GeneratedFile("xpidl.stub", script="header.py", entry_point="main") diff --git a/xpcom/idl-parser/xpidl/python.toml b/xpcom/idl-parser/xpidl/python.toml new file mode 100644 index 0000000000..577127329e --- /dev/null +++ b/xpcom/idl-parser/xpidl/python.toml @@ -0,0 +1,4 @@ +[DEFAULT] +subsuite = "xpcom" + +["runtests.py"] diff --git a/xpcom/idl-parser/xpidl/runtests.py b/xpcom/idl-parser/xpidl/runtests.py new file mode 100755 index 0000000000..2dd269dfd9 --- /dev/null +++ b/xpcom/idl-parser/xpidl/runtests.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python +# +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ +# +# Unit tests for xpidl.py + +import sys + +# Hack: the first entry in sys.path is the directory containing the script. +# This messes things up because that directory is the xpidl module, and that +# which conflicts with the xpidl submodule in the imports further below. +sys.path.pop(0) + +import unittest + +import mozunit + +from xpidl import header, xpidl + + +class TestParser(unittest.TestCase): + def setUp(self): + self.p = xpidl.IDLParser() + + def testEmpty(self): + i = self.p.parse("", filename="f") + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertEqual([], i.productions) + + def testForwardInterface(self): + i = self.p.parse("interface foo;", filename="f") + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertTrue(isinstance(i.productions[0], xpidl.Forward)) + self.assertEqual("foo", i.productions[0].name) + + def testInterface(self): + i = self.p.parse("[uuid(abc)] interface foo {};", filename="f") + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertTrue(isinstance(i.productions[0], xpidl.Interface)) + self.assertEqual("foo", i.productions[0].name) + + def testAttributes(self): + i = self.p.parse( + "[scriptable, builtinclass, function, uuid(abc)] interface foo {};", + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertTrue(isinstance(i.productions[0], xpidl.Interface)) + iface = i.productions[0] + self.assertEqual("foo", iface.name) + self.assertTrue(iface.attributes.scriptable) + self.assertTrue(iface.attributes.builtinclass) + self.assertTrue(iface.attributes.function) + + i = self.p.parse("[uuid(abc)] interface foo {};", filename="f") + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertTrue(isinstance(i.productions[0], xpidl.Interface)) + iface = i.productions[0] + self.assertEqual("foo", iface.name) + self.assertFalse(iface.attributes.scriptable) + + def testMethod(self): + i = self.p.parse( + """[uuid(abc)] interface foo { +void bar(); +};""", + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertTrue(isinstance(i.productions[0], xpidl.Interface)) + iface = i.productions[0] + m = iface.members[0] + self.assertTrue(isinstance(m, xpidl.Method)) + self.assertEqual("bar", m.name) + self.assertEqual(xpidl.TypeId("void"), m.type) + + def testMethodParams(self): + i = self.p.parse( + """ + [scriptable, uuid(aaa)] interface nsISupports {}; + [uuid(abc)] interface foo : nsISupports { + long bar(in long a, in float b, [array] in long c); + };""", + filename="f", + ) + i.resolve([], self.p, {}) + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertTrue(isinstance(i.productions[1], xpidl.Interface)) + iface = i.productions[1] + m = iface.members[0] + self.assertTrue(isinstance(m, xpidl.Method)) + self.assertEqual("bar", m.name) + self.assertEqual(xpidl.TypeId("long"), m.type) + self.assertEqual(3, len(m.params)) + self.assertEqual(xpidl.TypeId("long"), m.params[0].type) + self.assertEqual("in", m.params[0].paramtype) + self.assertEqual(xpidl.TypeId("float"), m.params[1].type) + self.assertEqual("in", m.params[1].paramtype) + self.assertEqual(xpidl.TypeId("long"), m.params[2].type) + self.assertEqual("in", m.params[2].paramtype) + self.assertTrue(isinstance(m.params[2].realtype, xpidl.LegacyArray)) + self.assertEqual("long", m.params[2].realtype.type.name) + + def testAttribute(self): + i = self.p.parse( + """[uuid(abc)] interface foo { +attribute long bar; +};""", + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + self.assertTrue(isinstance(i.productions[0], xpidl.Interface)) + iface = i.productions[0] + a = iface.members[0] + self.assertTrue(isinstance(a, xpidl.Attribute)) + self.assertEqual("bar", a.name) + self.assertEqual(xpidl.TypeId("long"), a.type) + + def testOverloadedVirtual(self): + i = self.p.parse( + """ + [scriptable, uuid(00000000-0000-0000-0000-000000000000)] interface nsISupports {}; + [uuid(abc)] interface foo : nsISupports { + attribute long bar; + void getBar(); + };""", + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + i.resolve([], self.p, {}) + + class FdMock: + def write(self, s): + pass + + try: + header.print_header(i, FdMock(), filename="f", relpath="f") + self.assertTrue(False, "Header printing failed to fail") + except Exception as e: + self.assertEqual( + e.args[0], + "Unexpected overloaded virtual method GetBar in interface foo", + ) + + def testNotISupports(self): + i = self.p.parse( + """ + [uuid(abc)] interface foo {}; + """, + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + try: + i.resolve([], self.p, {}) + self.assertTrue( + False, "Must check that interfaces inherit from nsISupports" + ) + except xpidl.IDLError as e: + self.assertEqual(e.args[0], "Interface 'foo' must inherit from nsISupports") + + def testBuiltinClassParent(self): + i = self.p.parse( + """ + [scriptable, uuid(aaa)] interface nsISupports {}; + [scriptable, builtinclass, uuid(abc)] interface foo : nsISupports {}; + [scriptable, uuid(def)] interface bar : foo {}; + """, + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + try: + i.resolve([], self.p, {}) + self.assertTrue( + False, "non-builtinclasses can't inherit from builtinclasses" + ) + except xpidl.IDLError as e: + self.assertEqual( + e.args[0], + "interface 'bar' is not builtinclass but derives from builtinclass 'foo'", + ) + + def testScriptableNotXPCOM(self): + # notxpcom method requires builtinclass on the interface + i = self.p.parse( + """ + [scriptable, uuid(aaa)] interface nsISupports {}; + [scriptable, uuid(abc)] interface nsIScriptableWithNotXPCOM : nsISupports { + [notxpcom] void method2(); + }; + """, + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + try: + i.resolve([], self.p, {}) + self.assertTrue( + False, + "Resolve should fail for non-builtinclasses with notxpcom methods", + ) + except xpidl.IDLError as e: + self.assertEqual( + e.args[0], + ( + "scriptable interface 'nsIScriptableWithNotXPCOM' " + "must be marked [builtinclass] because it contains a [notxpcom] " + "method 'method2'" + ), + ) + + # notxpcom attribute requires builtinclass on the interface + i = self.p.parse( + """ + interface nsISomeInterface; + [scriptable, uuid(aaa)] interface nsISupports {}; + [scriptable, uuid(abc)] interface nsIScriptableWithNotXPCOM : nsISupports { + [notxpcom] attribute nsISomeInterface attrib; + }; + """, + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + try: + i.resolve([], self.p, {}) + self.assertTrue( + False, + "Resolve should fail for non-builtinclasses with notxpcom attributes", + ) + except xpidl.IDLError as e: + self.assertEqual( + e.args[0], + ( + "scriptable interface 'nsIScriptableWithNotXPCOM' must be marked " + "[builtinclass] because it contains a [notxpcom] attribute 'attrib'" + ), + ) + + def testUndefinedConst(self): + i = self.p.parse( + """ + [scriptable, uuid(aaa)] interface nsISupports {}; + [scriptable, uuid(abc)] interface foo : nsISupports { + const unsigned long X = Y + 1; + }; + """, + filename="f", + ) + self.assertTrue(isinstance(i, xpidl.IDL)) + try: + i.resolve([], self.p, {}) + self.assertTrue(False, "Must detect undefined symbols") + except xpidl.IDLError as e: + self.assertEqual(e.args[0], ("cannot find symbol 'Y'")) + + +if __name__ == "__main__": + mozunit.main(runwith="unittest") diff --git a/xpcom/idl-parser/xpidl/rust.py b/xpcom/idl-parser/xpidl/rust.py new file mode 100644 index 0000000000..b9fc6627fb --- /dev/null +++ b/xpcom/idl-parser/xpidl/rust.py @@ -0,0 +1,693 @@ +# 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: &'static %(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 by default, as + /// XPCOM is generally not threadsafe. + /// + /// If this type is marked as [rust_sync], there will be explicit `Send` and + /// `Sync` implementations on this type, which will override the inherited + /// negative impls from `Rc`. + __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) + } +} +""" + + +sendsync_tmpl = """\ +// This interface is marked as [rust_sync], meaning it is safe to be transferred +// and used from multiple threads silmultaneously. These override the default +// from the __nosync marker type allowng the type to be sent between threads. +unsafe impl Send for %(name)s {} +unsafe impl Sync for %(name)s {} +""" + + +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.attributes.rust_sync: + fd.write(sendsync_tmpl % {"name": iface.name}) + + 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(), + } + ) + if type(member) == xpidl.CEnum: + for var in member.variants: + consts.append( + const_wrapper_tmpl + % { + "docs": "", + "type": member.rustType("in"), + "name": var.name, + "val": var.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), + } + ) diff --git a/xpcom/idl-parser/xpidl/rust_macros.py b/xpcom/idl-parser/xpidl/rust_macros.py new file mode 100644 index 0000000000..bc1788b96c --- /dev/null +++ b/xpcom/idl-parser/xpidl/rust_macros.py @@ -0,0 +1,111 @@ +# rust_macros.py - Generate rust_macros 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/. + +"""Generate rust bindings information for the IDL file specified""" + +from xpidl import rust, xpidl + +derive_method_tmpl = """\ +Method { + name: "%(name)s", + params: &[%(params)s], + ret: "%(ret)s", +}""" + + +def attrAsMethodStruct(iface, m, getter): + params = [ + 'Param { name: "%s", ty: "%s" }' % x + for x in rust.attributeRawParamList(iface, m, getter) + ] + return derive_method_tmpl % { + "name": rust.attributeNativeName(m, getter), + "params": ", ".join(params), + "ret": "::nserror::nsresult", + } + + +def methodAsMethodStruct(iface, m): + params = [ + 'Param { name: "%s", ty: "%s" }' % x for x in rust.methodRawParamList(iface, m) + ] + return derive_method_tmpl % { + "name": rust.methodNativeName(m), + "params": ", ".join(params), + "ret": rust.methodReturnType(m), + } + + +derive_iface_tmpl = """\ +Interface { + name: "%(name)s", + base: %(base)s, + sync: %(sync)s, + methods: %(methods)s, +}, +""" + + +def write_interface(iface, fd): + if iface.namemap is None: + raise Exception("Interface was not resolved.") + + assert iface.base or (iface.name == "nsISupports") + + base = 'Some("%s")' % iface.base if iface.base is not None else "None" + try: + methods = "" + for member in iface.members: + if type(member) == xpidl.Attribute: + methods += "/* %s */\n" % member.toIDL() + methods += "%s,\n" % attrAsMethodStruct(iface, member, True) + if not member.readonly: + methods += "%s,\n" % attrAsMethodStruct(iface, member, False) + methods += "\n" + + elif type(member) == xpidl.Method: + methods += "/* %s */\n" % member.toIDL() + methods += "%s,\n\n" % methodAsMethodStruct(iface, member) + fd.write( + derive_iface_tmpl + % { + "name": iface.name, + "base": base, + "sync": "true" if iface.attributes.rust_sync else "false", + "methods": "Ok(&[\n%s])" % methods, + } + ) + except xpidl.RustNoncompat as reason: + fd.write( + derive_iface_tmpl + % { + "name": iface.name, + "base": base, + "sync": "false", + "methods": 'Err("%s")' % reason, + } + ) + + +header = """\ +// +// DO NOT EDIT. THIS FILE IS GENERATED FROM $SRCDIR/%(relpath)s +// + +""" + + +def print_rust_macros_bindings(idl, fd, relpath): + fd = rust.AutoIndent(fd) + + fd.write(header % {"relpath": relpath}) + fd.write("{static D: &[Interface] = &[\n") + + for p in idl.productions: + if p.kind == "interface": + write_interface(p, fd) + + fd.write("]; D}\n") diff --git a/xpcom/idl-parser/xpidl/xpidl.py b/xpcom/idl-parser/xpidl/xpidl.py new file mode 100755 index 0000000000..b95fd14bc5 --- /dev/null +++ b/xpcom/idl-parser/xpidl/xpidl.py @@ -0,0 +1,2124 @@ +#!/usr/bin/env python +# xpidl.py - A parser for cross-platform IDL (XPIDL) files. +# +# 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/. + +"""A parser for cross-platform IDL (XPIDL) files.""" + +import os.path +import re +import sys +import textwrap +from collections import namedtuple + +import six +from ply import lex, yacc + +"""A type conforms to the following pattern: + + def nativeType(self, calltype): + 'returns a string representation of the native type + calltype must be 'in', 'out', 'inout', or 'element' + +Interface members const/method/attribute conform to the following pattern: + + name = 'string' + + def toIDL(self): + 'returns the member signature as IDL' +""" + + +# XXX(nika): Fix the IDL files which do this so we can remove this list? +def rustPreventForward(s): + """These types are foward declared as interfaces, but never actually defined + in IDL files. We don't want to generate references to them in rust for that + reason.""" + return s in ( + "nsIFrame", + "nsSubDocumentFrame", + ) + + +def attlistToIDL(attlist): + if len(attlist) == 0: + return "" + + attlist = list(attlist) + attlist.sort(key=lambda a: a[0]) + + return "[%s] " % ",".join( + [ + "%s%s" % (name, value is not None and "(%s)" % value or "") + for name, value, aloc in attlist + ] + ) + + +_paramsHardcode = { + 2: ("array", "shared", "iid_is", "size_is", "retval"), + 3: ("array", "size_is", "const"), +} + + +def paramAttlistToIDL(attlist): + if len(attlist) == 0: + return "" + + # Hack alert: g_hash_table_foreach is pretty much unimitatable... hardcode + # quirk + attlist = list(attlist) + sorted = [] + if len(attlist) in _paramsHardcode: + for p in _paramsHardcode[len(attlist)]: + i = 0 + while i < len(attlist): + if attlist[i][0] == p: + sorted.append(attlist[i]) + del attlist[i] + continue + + i += 1 + + sorted.extend(attlist) + + return "[%s] " % ", ".join( + [ + "%s%s" % (name, value is not None and " (%s)" % value or "") + for name, value, aloc in sorted + ] + ) + + +def unaliasType(t): + while t.kind == "typedef": + t = t.realtype + assert t is not None + return t + + +def getBuiltinOrNativeTypeName(t): + t = unaliasType(t) + if t.kind == "builtin": + return t.name + elif t.kind == "native": + assert t.specialtype is not None + return "[%s]" % t.specialtype + else: + return None + + +class BuiltinLocation(object): + def get(self): + return "<builtin type>" + + def __str__(self): + return self.get() + + +class Builtin(object): + kind = "builtin" + location = BuiltinLocation + + def __init__(self, name, nativename, rustname, signed=False, maybeConst=False): + self.name = name + self.nativename = nativename + self.rustname = rustname + self.signed = signed + self.maybeConst = maybeConst + + def isPointer(self): + """Check if this type is a pointer type - this will control how pointers act""" + return self.nativename.endswith("*") + + def nativeType(self, calltype, shared=False, const=False): + if self.name in ["string", "wstring"] and calltype == "element": + raise IDLError( + "Use string class types for string Array elements", self.location + ) + + if const: + print( + IDLError( + "[const] doesn't make sense on builtin types.", + self.location, + warning=True, + ), + file=sys.stderr, + ) + const = "const " + elif calltype == "in" and self.isPointer(): + const = "const " + elif shared: + if not self.isPointer(): + raise IDLError( + "[shared] not applicable to non-pointer types.", self.location + ) + const = "const " + else: + const = "" + return "%s%s %s" % (const, self.nativename, "*" if "out" in calltype else "") + + def rustType(self, calltype, shared=False, const=False): + # We want to rewrite any *mut pointers to *const pointers if constness + # was requested. + const = const or ("out" not in calltype and self.isPointer()) or shared + rustname = self.rustname + if const and self.isPointer(): + rustname = self.rustname.replace("*mut", "*const") + + return "%s%s" % ("*mut " if "out" in calltype else "", rustname) + + +builtinNames = [ + Builtin("boolean", "bool", "bool"), + Builtin("void", "void", "libc::c_void"), + Builtin("octet", "uint8_t", "u8", False, True), + Builtin("short", "int16_t", "i16", True, True), + Builtin("long", "int32_t", "i32", True, True), + Builtin("long long", "int64_t", "i64", True, True), + Builtin("unsigned short", "uint16_t", "u16", False, True), + Builtin("unsigned long", "uint32_t", "u32", False, True), + Builtin("unsigned long long", "uint64_t", "u64", False, True), + Builtin("float", "float", "libc::c_float", True, False), + Builtin("double", "double", "libc::c_double", True, False), + Builtin("char", "char", "libc::c_char", True, False), + Builtin("string", "char *", "*const libc::c_char", False, False), + Builtin("wchar", "char16_t", "u16", False, False), + Builtin("wstring", "char16_t *", "*const u16", False, False), + # As seen in mfbt/RefCountType.h, this type has special handling to + # maintain binary compatibility with MSCOM's IUnknown that cannot be + # expressed in XPIDL. + Builtin( + "MozExternalRefCountType", "MozExternalRefCountType", "MozExternalRefCountType" + ), +] + +builtinMap = {} +for b in builtinNames: + builtinMap[b.name] = b + + +class Location(object): + _line = None + + def __init__(self, lexer, lineno, lexpos): + self._lineno = lineno + self._lexpos = lexpos + self._lexdata = lexer.lexdata + self._file = getattr(lexer, "filename", "<unknown>") + + def __eq__(self, other): + return self._lexpos == other._lexpos and self._file == other._file + + def resolve(self): + if self._line: + return + + startofline = self._lexdata.rfind("\n", 0, self._lexpos) + 1 + endofline = self._lexdata.find("\n", self._lexpos, self._lexpos + 80) + self._line = self._lexdata[startofline:endofline] + self._colno = self._lexpos - startofline + + def pointerline(self): + def i(): + for i in range(0, self._colno): + yield " " + yield "^" + + return "".join(i()) + + def get(self): + self.resolve() + return "%s line %s:%s" % (self._file, self._lineno, self._colno) + + def __str__(self): + self.resolve() + return "%s line %s:%s\n%s\n%s" % ( + self._file, + self._lineno, + self._colno, + self._line, + self.pointerline(), + ) + + +class NameMap(object): + """Map of name -> object. Each object must have a .name and .location property. + Setting the same name twice throws an error.""" + + def __init__(self): + self._d = {} + + def __getitem__(self, key): + if key in builtinMap: + return builtinMap[key] + return self._d[key] + + def __iter__(self): + return six.itervalues(self._d) + + def __contains__(self, key): + return key in builtinMap or key in self._d + + def set(self, object): + if object.name in builtinMap: + raise IDLError( + "name '%s' is a builtin and cannot be redeclared" % (object.name), + object.location, + ) + if object.name.startswith("_"): + object.name = object.name[1:] + if object.name in self._d: + old = self._d[object.name] + if old == object: + return + if isinstance(old, Forward) and isinstance(object, Interface): + self._d[object.name] = object + elif isinstance(old, Interface) and isinstance(object, Forward): + pass + else: + raise IDLError( + "name '%s' specified twice. Previous location: %s" + % (object.name, self._d[object.name].location), + object.location, + ) + else: + self._d[object.name] = object + + def get(self, id, location): + try: + return self[id] + except KeyError: + raise IDLError(f"Name '{id}' not found", location) + + +class RustNoncompat(Exception): + """ + This exception is raised when a particular type or function cannot be safely exposed to + rust code + """ + + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return self.reason + + +class IDLError(Exception): + def __init__(self, message, location, warning=False, notes=None): + self.message = message + self.location = location + self.warning = warning + self.notes = notes + + def __str__(self): + error = "%s: %s, %s" % ( + self.warning and "warning" or "error", + self.message, + self.location, + ) + if self.notes is not None: + error += "\nnote: %s" % self.notes + return error + + +class Include(object): + kind = "include" + + def __init__(self, filename, location): + self.filename = filename + self.location = location + + def __str__(self): + return "".join(["include '%s'\n" % self.filename]) + + def resolve(self, parent): + def incfiles(): + yield self.filename + for dir in parent.incdirs: + yield os.path.join(dir, self.filename) + + for file in incfiles(): + if not os.path.exists(file): + continue + + if file in parent.includeCache: + self.IDL = parent.includeCache[file] + else: + self.IDL = parent.parser.parse( + open(file, encoding="utf-8").read(), filename=file + ) + self.IDL.resolve( + parent.incdirs, + parent.parser, + parent.webidlconfig, + parent.includeCache, + ) + parent.includeCache[file] = self.IDL + + for type in self.IDL.getNames(): + parent.setName(type) + parent.deps.extend(self.IDL.deps) + return + + raise IDLError("File '%s' not found" % self.filename, self.location) + + +class IDL(object): + def __init__(self, productions): + self.hasSequence = False + self.productions = productions + self.deps = [] + + def setName(self, object): + self.namemap.set(object) + + def getName(self, id, location): + if id.name == "Array": + if id.params is None or len(id.params) != 1: + raise IDLError("Array takes exactly 1 parameter", location) + self.hasSequence = True + return Array(self.getName(id.params[0], location), location) + + if id.params is not None: + raise IDLError("Generic type '%s' unrecognized" % id.name, location) + + try: + return self.namemap[id.name] + except KeyError: + raise IDLError("type '%s' not found" % id.name, location) + + def hasName(self, id): + return id in self.namemap + + def getNames(self): + return iter(self.namemap) + + def __str__(self): + return "".join([str(p) for p in self.productions]) + + def resolve(self, incdirs, parser, webidlconfig, includeCache=None): + self.namemap = NameMap() + self.incdirs = incdirs + self.parser = parser + self.webidlconfig = webidlconfig + self.includeCache = {} if includeCache is None else includeCache + for p in self.productions: + p.resolve(self) + + def includes(self): + for p in self.productions: + if p.kind == "include": + yield p + if self.hasSequence: + yield Include("nsTArray.h", BuiltinLocation) + + def needsJSTypes(self): + for p in self.productions: + if p.kind == "interface" and p.needsJSTypes(): + return True + return False + + +class CDATA(object): + kind = "cdata" + _re = re.compile(r"\n+") + + def __init__(self, data, location): + self.data = self._re.sub("\n", data) + self.location = location + + def resolve(self, parent): + # This can be a false-positive if the word `virtual` is included in a + # comment, however this doesn't seem to happen very often. + if isinstance(parent, Interface) and re.search(r"\bvirtual\b", self.data): + raise IDLError( + "cannot declare a C++ `virtual` member in XPIDL interface", + self.location, + notes=textwrap.fill( + """All virtual members must be declared directly using XPIDL. + Both the Rust bindings and XPConnect rely on the per-platform + vtable layouts generated by the XPIDL compiler to allow + cross-language XPCOM method calls between JS and C++. + Consider using a `[notxpcom, nostdcall]` method instead.""" + ), + ) + + def __str__(self): + return "cdata: %s\n\t%r\n" % (self.location.get(), self.data) + + def count(self): + return 0 + + +class Typedef(object): + kind = "typedef" + + def __init__(self, type, name, location, doccomments): + self.type = type + self.name = name + self.location = location + self.doccomments = doccomments + + def __eq__(self, other): + return self.name == other.name and self.type == other.type + + def resolve(self, parent): + parent.setName(self) + self.realtype = parent.getName(self.type, self.location) + + if not isinstance(self.realtype, (Builtin, CEnum, Native, Typedef)): + raise IDLError("Unsupported typedef target type", self.location) + + def nativeType(self, calltype): + return "%s %s" % (self.name, "*" if "out" in calltype else "") + + def rustType(self, calltype): + if self.name == "nsresult": + return "%s::nserror::nsresult" % ("*mut " if "out" in calltype else "") + + return "%s%s" % ("*mut " if "out" in calltype else "", self.name) + + def __str__(self): + return "typedef %s %s\n" % (self.type, self.name) + + +class Forward(object): + kind = "forward" + + def __init__(self, name, location, doccomments): + self.name = name + self.location = location + self.doccomments = doccomments + + def __eq__(self, other): + return other.kind == "forward" and other.name == self.name + + def resolve(self, parent): + # Hack alert: if an identifier is already present, move the doccomments + # forward. + if parent.hasName(self.name): + for i in range(0, len(parent.productions)): + if parent.productions[i] is self: + break + for i in range(i + 1, len(parent.productions)): + if hasattr(parent.productions[i], "doccomments"): + parent.productions[i].doccomments[0:0] = self.doccomments + break + + parent.setName(self) + + def nativeType(self, calltype): + if calltype == "element": + return "RefPtr<%s>" % self.name + return "%s *%s" % (self.name, "*" if "out" in calltype else "") + + def rustType(self, calltype): + if rustPreventForward(self.name): + raise RustNoncompat("forward declaration %s is unsupported" % self.name) + if calltype == "element": + return "Option<RefPtr<%s>>" % self.name + return "%s*const %s" % ("*mut" if "out" in calltype else "", self.name) + + def __str__(self): + return "forward-declared %s\n" % self.name + + +class Native(object): + kind = "native" + + modifier = None + specialtype = None + + # A tuple type here means that a custom value is used for each calltype: + # (in, out/inout, array element) respectively. + # A `None` here means that the written type should be used as-is. + specialtypes = { + "nsid": None, + "utf8string": ("const nsACString&", "nsACString&", "nsCString"), + "cstring": ("const nsACString&", "nsACString&", "nsCString"), + "astring": ("const nsAString&", "nsAString&", "nsString"), + "jsval": ("JS::Handle<JS::Value>", "JS::MutableHandle<JS::Value>", "JS::Value"), + "promise": "::mozilla::dom::Promise", + } + + def __init__(self, name, nativename, attlist, location): + self.name = name + self.nativename = nativename + self.location = location + + for name, value, aloc in attlist: + if value is not None: + raise IDLError("Unexpected attribute value", aloc) + if name in ("ptr", "ref"): + if self.modifier is not None: + raise IDLError("More than one ptr/ref modifier", aloc) + self.modifier = name + elif name in self.specialtypes.keys(): + if self.specialtype is not None: + raise IDLError("More than one special type", aloc) + self.specialtype = name + if self.specialtypes[name] is not None: + self.nativename = self.specialtypes[name] + else: + raise IDLError("Unexpected attribute", aloc) + + def __eq__(self, other): + return ( + self.name == other.name + and self.nativename == other.nativename + and self.modifier == other.modifier + and self.specialtype == other.specialtype + ) + + def resolve(self, parent): + parent.setName(self) + + def isPtr(self, calltype): + return self.modifier == "ptr" + + def isRef(self, calltype): + return self.modifier == "ref" + + def nativeType(self, calltype, const=False, shared=False): + if shared: + if calltype != "out": + raise IDLError( + "[shared] only applies to out parameters.", self.location + ) + const = True + + if isinstance(self.nativename, tuple): + if calltype == "in": + return self.nativename[0] + " " + elif "out" in calltype: + return self.nativename[1] + " " + else: + return self.nativename[2] + " " + + # 'in' nsid parameters should be made 'const' + if self.specialtype == "nsid" and calltype == "in": + const = True + + if calltype == "element": + if self.specialtype == "nsid": + if self.isPtr(calltype): + raise IDLError( + "Array<nsIDPtr> not yet supported. " + "File an XPConnect bug if you need it.", + self.location, + ) + + # ns[CI]?IDs should be held directly in Array<T>s + return self.nativename + + if self.isRef(calltype): + raise IDLError( + "[ref] qualified type unsupported in Array<T>", self.location + ) + + # Promises should be held in RefPtr<T> in Array<T>s + if self.specialtype == "promise": + return "RefPtr<mozilla::dom::Promise>" + + if self.isRef(calltype): + m = "& " # [ref] is always passed with a single indirection + else: + m = "* " if "out" in calltype else "" + if self.isPtr(calltype): + m += "* " + return "%s%s %s" % (const and "const " or "", self.nativename, m) + + def rustType(self, calltype, const=False, shared=False): + # For the most part, 'native' types don't make sense in rust, as they + # are native C++ types. However, we can support a few types here, as + # they're important and can easily be translated. + # + # NOTE: This code doesn't try to perfectly match C++ constness, as + # constness doesn't affect ABI, and raw pointers are already unsafe. + + if self.modifier not in ["ptr", "ref"]: + raise RustNoncompat("Rust only supports [ref] / [ptr] native types") + + if shared: + if calltype != "out": + raise IDLError( + "[shared] only applies to out parameters.", self.location + ) + const = True + + # 'in' nsid parameters should be made 'const' + if self.specialtype == "nsid" and calltype == "in": + const = True + + prefix = "*const " if const or shared else "*mut " + if "out" in calltype and self.isPtr(calltype): + prefix = "*mut " + prefix + + if self.specialtype: + # The string types are very special, and need to be handled seperately. + if self.specialtype in ["cstring", "utf8string"]: + if calltype == "in": + return "*const ::nsstring::nsACString" + elif "out" in calltype: + return "*mut ::nsstring::nsACString" + else: + return "::nsstring::nsCString" + if self.specialtype == "astring": + if calltype == "in": + return "*const ::nsstring::nsAString" + elif "out" in calltype: + return "*mut ::nsstring::nsAString" + else: + return "::nsstring::nsString" + # nsid has some special handling, but generally re-uses the generic + # prefix handling above. + if self.specialtype == "nsid": + if "element" in calltype: + if self.isPtr(calltype): + raise IDLError( + "Array<nsIDPtr> not yet supported. " + "File an XPConnect bug if you need it.", + self.location, + ) + return self.nativename + return prefix + self.nativename + raise RustNoncompat("special type %s unsupported" % self.specialtype) + + # These 3 special types correspond to native pointer types which can + # generally be supported behind pointers. Other types are not supported + # for now. + if self.nativename == "void": + return prefix + "libc::c_void" + if self.nativename == "char": + return prefix + "libc::c_char" + if self.nativename == "char16_t": + return prefix + "u16" + + raise RustNoncompat("native type %s unsupported" % self.nativename) + + def __str__(self): + return "native %s(%s)\n" % (self.name, self.nativename) + + +class WebIDL(object): + kind = "webidl" + + def __init__(self, name, location): + self.name = name + self.location = location + + def __eq__(self, other): + return other.kind == "webidl" and self.name == other.name + + def resolve(self, parent): + # XXX(nika): We don't handle _every_ kind of webidl object here (as that + # would be hard). For example, we don't support nsIDOM*-defaulting + # interfaces. + # TODO: More explicit compile-time checks? + + assert ( + parent.webidlconfig is not None + ), "WebIDL declarations require passing webidlconfig to resolve." + + # Resolve our native name according to the WebIDL configs. + config = parent.webidlconfig.get(self.name, {}) + self.native = config.get("nativeType") + if self.native is None: + self.native = "mozilla::dom::%s" % self.name + self.headerFile = config.get("headerFile") + if self.headerFile is None: + self.headerFile = self.native.replace("::", "/") + ".h" + + parent.setName(self) + + def nativeType(self, calltype, const=False): + if calltype == "element": + return "RefPtr<%s%s>" % ("const " if const else "", self.native) + return "%s%s *%s" % ( + "const " if const else "", + self.native, + "*" if "out" in calltype else "", + ) + + def rustType(self, calltype, const=False): + # Just expose the type as a void* - we can't do any better. + return "%s*const libc::c_void" % ("*mut " if "out" in calltype else "") + + def __str__(self): + return "webidl %s\n" % self.name + + +class Interface(object): + kind = "interface" + + def __init__(self, name, attlist, base, members, location, doccomments): + self.name = name + self.attributes = InterfaceAttributes(attlist, location) + self.base = base + self.members = members + self.location = location + self.namemap = NameMap() + self.doccomments = doccomments + self.nativename = name + + for m in members: + if not isinstance(m, CDATA): + self.namemap.set(m) + + def __eq__(self, other): + return self.name == other.name and self.location == other.location + + def resolve(self, parent): + self.idl = parent + + if not self.attributes.scriptable and self.attributes.builtinclass: + raise IDLError( + "Non-scriptable interface '%s' doesn't need to be marked builtinclass" + % self.name, + self.location, + ) + + # Hack alert: if an identifier is already present, libIDL assigns + # doc comments incorrectly. This is quirks-mode extraordinaire! + if parent.hasName(self.name): + for member in self.members: + if hasattr(member, "doccomments"): + member.doccomments[0:0] = self.doccomments + break + self.doccomments = parent.getName(TypeId(self.name), None).doccomments + + if self.attributes.function: + has_method = False + for member in self.members: + if member.kind == "method": + if has_method: + raise IDLError( + "interface '%s' has multiple methods, but marked 'function'" + % self.name, + self.location, + ) + else: + has_method = True + + parent.setName(self) + if self.base is not None: + realbase = parent.getName(TypeId(self.base), self.location) + if realbase.kind != "interface": + raise IDLError( + "interface '%s' inherits from non-interface type '%s'" + % (self.name, self.base), + self.location, + ) + + if self.attributes.scriptable and not realbase.attributes.scriptable: + raise IDLError( + "interface '%s' is scriptable but derives from " + "non-scriptable '%s'" % (self.name, self.base), + self.location, + warning=True, + ) + + if ( + self.attributes.scriptable + and realbase.attributes.builtinclass + and not self.attributes.builtinclass + ): + raise IDLError( + "interface '%s' is not builtinclass but derives from " + "builtinclass '%s'" % (self.name, self.base), + self.location, + ) + + if realbase.attributes.rust_sync and not self.attributes.rust_sync: + raise IDLError( + "interface '%s' is not rust_sync but derives from rust_sync '%s'" + % (self.name, self.base), + self.location, + ) + + if ( + self.attributes.rust_sync + and self.attributes.scriptable + and not self.attributes.builtinclass + ): + raise IDLError( + "interface '%s' is rust_sync but is not builtinclass" % self.name, + self.location, + ) + elif self.name != "nsISupports": + raise IDLError( + "Interface '%s' must inherit from nsISupports" % self.name, + self.location, + ) + + for member in self.members: + member.resolve(self) + + # The number 250 is NOT arbitrary; this number is the maximum number of + # stub entries defined in xpcom/reflect/xptcall/genstubs.pl + # Do not increase this value without increasing the number in that + # location, or you WILL cause otherwise unknown problems! + if self.countEntries() > 250 and not self.attributes.builtinclass: + raise IDLError( + "interface '%s' has too many entries" % self.name, self.location + ) + + def nativeType(self, calltype, const=False): + if calltype == "element": + return "RefPtr<%s>" % self.name + return "%s%s *%s" % ( + "const " if const else "", + self.name, + "*" if "out" in calltype else "", + ) + + def rustType(self, calltype, const=False): + if calltype == "element": + return "Option<RefPtr<%s>>" % self.name + return "%s*const %s" % ("*mut " if "out" in calltype else "", self.name) + + def __str__(self): + l = ["interface %s\n" % self.name] + if self.base is not None: + l.append("\tbase %s\n" % self.base) + l.append(str(self.attributes)) + if self.members is None: + l.append("\tincomplete type\n") + else: + for m in self.members: + l.append(str(m)) + return "".join(l) + + def getConst(self, name, location): + # The constant may be in a base class + iface = self + while name not in iface.namemap and iface.base is not None: + iface = self.idl.getName(TypeId(iface.base), self.location) + if name not in iface.namemap: + raise IDLError("cannot find symbol '%s'" % name, location) + c = iface.namemap.get(name, location) + if c.kind != "const": + raise IDLError("symbol '%s' is not a constant" % name, location) + + return c.getValue() + + def needsJSTypes(self): + for m in self.members: + if m.kind == "attribute" and m.type == TypeId("jsval"): + return True + if m.kind == "method" and m.needsJSTypes(): + return True + return False + + def countEntries(self): + """Returns the number of entries in the vtable for this interface.""" + total = sum(member.count() for member in self.members) + if self.base is not None: + realbase = self.idl.getName(TypeId(self.base), self.location) + total += realbase.countEntries() + return total + + +class InterfaceAttributes(object): + uuid = None + scriptable = False + builtinclass = False + function = False + main_process_scriptable_only = False + rust_sync = False + + def setuuid(self, value): + self.uuid = value.lower() + + def setscriptable(self): + self.scriptable = True + + def setfunction(self): + self.function = True + + def setbuiltinclass(self): + self.builtinclass = True + + def setmain_process_scriptable_only(self): + self.main_process_scriptable_only = True + + def setrust_sync(self): + self.rust_sync = True + + actions = { + "uuid": (True, setuuid), + "scriptable": (False, setscriptable), + "builtinclass": (False, setbuiltinclass), + "function": (False, setfunction), + "object": (False, lambda self: True), + "main_process_scriptable_only": (False, setmain_process_scriptable_only), + "rust_sync": (False, setrust_sync), + } + + def __init__(self, attlist, location): + def badattribute(self): + raise IDLError("Unexpected interface attribute '%s'" % name, location) + + for name, val, aloc in attlist: + hasval, action = self.actions.get(name, (False, badattribute)) + if hasval: + if val is None: + raise IDLError("Expected value for attribute '%s'" % name, aloc) + + action(self, val) + else: + if val is not None: + raise IDLError("Unexpected value for attribute '%s'" % name, aloc) + + action(self) + + if self.uuid is None: + raise IDLError("interface has no uuid", location) + + def __str__(self): + l = [] + if self.uuid: + l.append("\tuuid: %s\n" % self.uuid) + if self.scriptable: + l.append("\tscriptable\n") + if self.builtinclass: + l.append("\tbuiltinclass\n") + if self.function: + l.append("\tfunction\n") + if self.main_process_scriptable_only: + l.append("\tmain_process_scriptable_only\n") + if self.rust_sync: + l.append("\trust_sync\n") + return "".join(l) + + +class ConstMember(object): + kind = "const" + + def __init__(self, type, name, value, location, doccomments): + self.type = type + self.name = name + self.valueFn = value + self.location = location + self.doccomments = doccomments + + def resolve(self, parent): + self.realtype = parent.idl.getName(self.type, self.location) + self.iface = parent + basetype = self.realtype + while isinstance(basetype, Typedef): + basetype = basetype.realtype + if not isinstance(basetype, Builtin) or not basetype.maybeConst: + raise IDLError( + "const may only be a short or long type, not %s" % self.type, + self.location, + ) + + self.basetype = basetype + # Value is a lambda. Resolve it. + self.value = self.valueFn(self.iface) + + min_val = -(2**31) if basetype.signed else 0 + max_val = 2**31 - 1 if basetype.signed else 2**32 - 1 + if self.value < min_val or self.value > max_val: + raise IDLError( + "xpidl constants must fit within %s" + % ("int32_t" if basetype.signed else "uint32_t"), + self.location, + ) + + def getValue(self): + return self.value + + def __str__(self): + return "\tconst %s %s = %s\n" % (self.type, self.name, self.getValue()) + + def count(self): + return 0 + + +# Represents a single name/value pair in a CEnum +class CEnumVariant(object): + # Treat CEnumVariants as consts in terms of value resolution, so we can + # do things like binary operation values for enum members. + kind = "const" + + def __init__(self, name, value, location): + self.name = name + self.valueFn = value + self.location = location + + def getValue(self): + return self.value + + +class CEnum(object): + kind = "cenum" + + def __init__(self, width, name, variants, location, doccomments): + # We have to set a name here, otherwise we won't pass namemap checks on + # the interface. This name will change it in resolve(), in order to + # namespace the enum within the interface. + self.name = name + self.basename = name + self.width = width + self.location = location + self.namemap = NameMap() + self.doccomments = doccomments + self.variants = variants + if self.width not in (8, 16, 32): + raise IDLError("Width must be one of {8, 16, 32}", self.location) + + def resolve(self, iface): + self.iface = iface + # Renaming enum to faux-namespace the enum type to the interface in JS + # so we don't collide in the global namespace. Hacky/ugly but it does + # the job well enough, and the name will still be interface::variant in + # C++. + self.name = "%s_%s" % (self.iface.name, self.basename) + self.iface.idl.setName(self) + + # Compute the value for each enum variant that doesn't set its own + # value + next_value = 0 + for variant in self.variants: + # CEnum variants resolve to interface level consts in javascript, + # meaning their names could collide with other interface members. + # Iterate through all CEnum variants to make sure there are no + # collisions. + self.iface.namemap.set(variant) + # Value may be a lambda. If it is, resolve it. + if variant.valueFn: + next_value = variant.value = variant.valueFn(self.iface) + else: + variant.value = next_value + next_value += 1 + + def count(self): + return 0 + + def nativeType(self, calltype): + if "out" in calltype: + return "%s::%s *" % (self.iface.name, self.basename) + return "%s::%s " % (self.iface.name, self.basename) + + def rustType(self, calltype): + return "%s u%d" % ("*mut" if "out" in calltype else "", self.width) + + def __str__(self): + body = ", ".join("%s = %s" % v for v in self.variants) + return "\tcenum %s : %d { %s };\n" % (self.name, self.width, body) + + +# Infallible doesn't work for all return types. +# +# It also must be implemented on a builtinclass (otherwise it'd be unsound as +# it could be implemented by JS). +def ensureInfallibleIsSound(methodOrAttribute): + if not methodOrAttribute.infallible: + return + if methodOrAttribute.realtype.kind not in [ + "builtin", + "interface", + "forward", + "webidl", + "cenum", + ]: + raise IDLError( + "[infallible] only works on interfaces, domobjects, and builtin types " + "(numbers, booleans, cenum, and raw char types)", + methodOrAttribute.location, + ) + ifaceAttributes = methodOrAttribute.iface.attributes + if ifaceAttributes.scriptable and not ifaceAttributes.builtinclass: + raise IDLError( + "[infallible] attributes and methods are only allowed on " + "non-[scriptable] or [builtinclass] interfaces", + methodOrAttribute.location, + ) + + if methodOrAttribute.notxpcom: + raise IDLError( + "[infallible] does not make sense for a [notxpcom] " "method or attribute", + methodOrAttribute.location, + ) + + +# An interface cannot be implemented by JS if it has a notxpcom or nostdcall +# method or attribute, so it must be marked as builtinclass. +def ensureBuiltinClassIfNeeded(methodOrAttribute): + iface = methodOrAttribute.iface + if not iface.attributes.scriptable or iface.attributes.builtinclass: + return + if iface.name == "nsISupports": + return + if methodOrAttribute.notxpcom: + raise IDLError( + ( + "scriptable interface '%s' must be marked [builtinclass] because it " + "contains a [notxpcom] %s '%s'" + ) + % (iface.name, methodOrAttribute.kind, methodOrAttribute.name), + methodOrAttribute.location, + ) + if methodOrAttribute.nostdcall: + raise IDLError( + ( + "scriptable interface '%s' must be marked [builtinclass] because it " + "contains a [nostdcall] %s '%s'" + ) + % (iface.name, methodOrAttribute.kind, methodOrAttribute.name), + methodOrAttribute.location, + ) + + +class Attribute(object): + kind = "attribute" + noscript = False + notxpcom = False + readonly = False + symbol = False + implicit_jscontext = False + nostdcall = False + must_use = False + binaryname = None + infallible = False + # explicit_setter_can_run_script is true if the attribute is explicitly + # annotated as having a setter that can cause script to run. + explicit_setter_can_run_script = False + # explicit_getter_can_run_script is true if the attribute is explicitly + # annotated as having a getter that can cause script to run. + explicit_getter_can_run_script = False + + def __init__(self, type, name, attlist, readonly, location, doccomments): + self.type = type + self.name = name + self.attlist = attlist + self.readonly = readonly + self.location = location + self.doccomments = doccomments + + for name, value, aloc in attlist: + if name == "binaryname": + if value is None: + raise IDLError("binaryname attribute requires a value", aloc) + + self.binaryname = value + continue + + if value is not None: + raise IDLError("Unexpected attribute value", aloc) + + if name == "noscript": + self.noscript = True + elif name == "notxpcom": + self.notxpcom = True + elif name == "symbol": + self.symbol = True + elif name == "implicit_jscontext": + self.implicit_jscontext = True + elif name == "nostdcall": + self.nostdcall = True + elif name == "must_use": + self.must_use = True + elif name == "infallible": + self.infallible = True + elif name == "can_run_script": + if ( + self.explicit_setter_can_run_script + or self.explicit_getter_can_run_script + ): + raise IDLError( + "Redundant getter_can_run_script or " + "setter_can_run_script annotation on " + "attribute", + aloc, + ) + self.explicit_setter_can_run_script = True + self.explicit_getter_can_run_script = True + elif name == "setter_can_run_script": + if self.explicit_setter_can_run_script: + raise IDLError( + "Redundant setter_can_run_script annotation " "on attribute", + aloc, + ) + self.explicit_setter_can_run_script = True + elif name == "getter_can_run_script": + if self.explicit_getter_can_run_script: + raise IDLError( + "Redundant getter_can_run_script annotation " "on attribute", + aloc, + ) + self.explicit_getter_can_run_script = True + else: + raise IDLError("Unexpected attribute '%s'" % name, aloc) + + def resolve(self, iface): + self.iface = iface + self.realtype = iface.idl.getName(self.type, self.location) + + ensureInfallibleIsSound(self) + ensureBuiltinClassIfNeeded(self) + + def toIDL(self): + attribs = attlistToIDL(self.attlist) + readonly = self.readonly and "readonly " or "" + return "%s%sattribute %s %s;" % (attribs, readonly, self.type, self.name) + + def isScriptable(self): + if not self.iface.attributes.scriptable: + return False + return not (self.noscript or self.notxpcom or self.nostdcall) + + def __str__(self): + return "\t%sattribute %s %s\n" % ( + self.readonly and "readonly " or "", + self.type, + self.name, + ) + + def count(self): + return self.readonly and 1 or 2 + + +class Method(object): + kind = "method" + noscript = False + notxpcom = False + symbol = False + binaryname = None + implicit_jscontext = False + nostdcall = False + must_use = False + optional_argc = False + # explicit_can_run_script is true if the method is explicitly annotated + # as being able to cause script to run. + explicit_can_run_script = False + infallible = False + + def __init__(self, type, name, attlist, paramlist, location, doccomments, raises): + self.type = type + self.name = name + self.attlist = attlist + self.params = paramlist + self.location = location + self.doccomments = doccomments + self.raises = raises + + for name, value, aloc in attlist: + if name == "binaryname": + if value is None: + raise IDLError("binaryname attribute requires a value", aloc) + + self.binaryname = value + continue + + if value is not None: + raise IDLError("Unexpected attribute value", aloc) + + if name == "noscript": + self.noscript = True + elif name == "notxpcom": + self.notxpcom = True + elif name == "symbol": + self.symbol = True + elif name == "implicit_jscontext": + self.implicit_jscontext = True + elif name == "optional_argc": + self.optional_argc = True + elif name == "nostdcall": + self.nostdcall = True + elif name == "must_use": + self.must_use = True + elif name == "can_run_script": + self.explicit_can_run_script = True + elif name == "infallible": + self.infallible = True + else: + raise IDLError("Unexpected attribute '%s'" % name, aloc) + + self.namemap = NameMap() + for p in paramlist: + self.namemap.set(p) + + def resolve(self, iface): + self.iface = iface + self.realtype = self.iface.idl.getName(self.type, self.location) + + ensureInfallibleIsSound(self) + ensureBuiltinClassIfNeeded(self) + + for p in self.params: + p.resolve(self) + for p in self.params: + if p.retval and p != self.params[-1]: + raise IDLError( + "'retval' parameter '%s' is not the last parameter" % p.name, + self.location, + ) + if p.size_is: + size_param = self.namemap.get(p.size_is, p.location) + if ( + p.paramtype.count("in") == 1 + and size_param.paramtype.count("in") == 0 + ): + raise IDLError( + "size_is parameter of an input must also be an input", + p.location, + ) + if getBuiltinOrNativeTypeName(size_param.realtype) != "unsigned long": + raise IDLError( + "size_is parameter must have type 'unsigned long'", + p.location, + ) + if p.iid_is: + iid_param = self.namemap.get(p.iid_is, p.location) + if ( + p.paramtype.count("in") == 1 + and iid_param.paramtype.count("in") == 0 + ): + raise IDLError( + "iid_is parameter of an input must also be an input", + p.location, + ) + if getBuiltinOrNativeTypeName(iid_param.realtype) != "[nsid]": + raise IDLError( + "iid_is parameter must be an nsIID", + self.location, + ) + + def isScriptable(self): + if not self.iface.attributes.scriptable: + return False + return not (self.noscript or self.notxpcom or self.nostdcall) + + def __str__(self): + return "\t%s %s(%s)\n" % ( + self.type, + self.name, + ", ".join([p.name for p in self.params]), + ) + + def toIDL(self): + if len(self.raises): + raises = " raises (%s)" % ",".join(self.raises) + else: + raises = "" + + return "%s%s %s (%s)%s;" % ( + attlistToIDL(self.attlist), + self.type, + self.name, + ", ".join([p.toIDL() for p in self.params]), + raises, + ) + + def needsJSTypes(self): + if self.implicit_jscontext: + return True + if self.type == TypeId("jsval"): + return True + for p in self.params: + t = p.realtype + if isinstance(t, Native) and t.specialtype == "jsval": + return True + return False + + def count(self): + return 1 + + +class Param(object): + size_is = None + iid_is = None + const = False + array = False + retval = False + shared = False + optional = False + default_value = None + + def __init__(self, paramtype, type, name, attlist, location, realtype=None): + self.paramtype = paramtype + self.type = type + self.name = name + self.attlist = attlist + self.location = location + self.realtype = realtype + + for name, value, aloc in attlist: + # Put the value-taking attributes first! + if name == "size_is": + if value is None: + raise IDLError("'size_is' must specify a parameter", aloc) + self.size_is = value + elif name == "iid_is": + if value is None: + raise IDLError("'iid_is' must specify a parameter", aloc) + self.iid_is = value + elif name == "default": + if value is None: + raise IDLError("'default' must specify a default value", aloc) + self.default_value = value + else: + if value is not None: + raise IDLError("Unexpected value for attribute '%s'" % name, aloc) + + if name == "const": + self.const = True + elif name == "array": + self.array = True + elif name == "retval": + self.retval = True + elif name == "shared": + self.shared = True + elif name == "optional": + self.optional = True + else: + raise IDLError("Unexpected attribute '%s'" % name, aloc) + + def resolve(self, method): + self.realtype = method.iface.idl.getName(self.type, self.location) + if self.array: + self.realtype = LegacyArray(self.realtype) + + def nativeType(self): + kwargs = {} + if self.shared: + kwargs["shared"] = True + if self.const: + kwargs["const"] = True + + try: + return self.realtype.nativeType(self.paramtype, **kwargs) + except IDLError as e: + raise IDLError(str(e), self.location) + except TypeError: + raise IDLError("Unexpected parameter attribute", self.location) + + def rustType(self): + kwargs = {} + if self.shared: + kwargs["shared"] = True + if self.const: + kwargs["const"] = True + + try: + return self.realtype.rustType(self.paramtype, **kwargs) + except IDLError as e: + raise IDLError(str(e), self.location) + except TypeError: + raise IDLError("Unexpected parameter attribute", self.location) + + def toIDL(self): + return "%s%s %s %s" % ( + paramAttlistToIDL(self.attlist), + self.paramtype, + self.type, + self.name, + ) + + +class LegacyArray(object): + def __init__(self, basetype): + self.type = basetype + self.location = self.type.location + + def nativeType(self, calltype, const=False): + if "element" in calltype: + raise IDLError("nested [array] unsupported", self.location) + + # For legacy reasons, we have to add a 'const ' to builtin pointer array + # types. (`[array] in string` and `[array] in wstring` parameters) + if ( + calltype == "in" + and isinstance(self.type, Builtin) + and self.type.isPointer() + ): + const = True + + return "%s%s*%s" % ( + "const " if const else "", + self.type.nativeType("legacyelement"), + "*" if "out" in calltype else "", + ) + + def rustType(self, calltype, const=False): + return "%s%s%s" % ( + "*mut " if "out" in calltype else "", + "*const " if const else "*mut ", + self.type.rustType("legacyelement"), + ) + + +class Array(object): + kind = "array" + + def __init__(self, type, location): + self.type = type + self.location = location + + @property + def name(self): + return "Array<%s>" % self.type.name + + def resolve(self, idl): + idl.getName(self.type, self.location) + + def nativeType(self, calltype): + if calltype == "legacyelement": + raise IDLError("[array] Array<T> is unsupported", self.location) + + base = "nsTArray<%s>" % self.type.nativeType("element") + if "out" in calltype: + return "%s& " % base + elif "in" == calltype: + return "const %s& " % base + else: + return base + + def rustType(self, calltype): + if calltype == "legacyelement": + raise IDLError("[array] Array<T> is unsupported", self.location) + + base = "thin_vec::ThinVec<%s>" % self.type.rustType("element") + if "out" in calltype: + return "*mut %s" % base + elif "in" == calltype: + return "*const %s" % base + else: + return base + + +TypeId = namedtuple("TypeId", "name params") + + +# Make str(TypeId) produce a nicer value +TypeId.__str__ = ( + lambda self: "%s<%s>" % (self.name, ", ".join(str(p) for p in self.params)) + if self.params is not None + else self.name +) + + +# Allow skipping 'params' in TypeId(..) +TypeId.__new__.__defaults__ = (None,) + + +class IDLParser(object): + keywords = { + "cenum": "CENUM", + "const": "CONST", + "interface": "INTERFACE", + "in": "IN", + "inout": "INOUT", + "out": "OUT", + "attribute": "ATTRIBUTE", + "raises": "RAISES", + "readonly": "READONLY", + "native": "NATIVE", + "typedef": "TYPEDEF", + "webidl": "WEBIDL", + } + + tokens = [ + "IDENTIFIER", + "CDATA", + "INCLUDE", + "IID", + "NUMBER", + "HEXNUM", + "LSHIFT", + "RSHIFT", + "NATIVEID", + ] + + tokens.extend(keywords.values()) + + states = (("nativeid", "exclusive"),) + + hexchar = r"[a-fA-F0-9]" + + t_NUMBER = r"-?\d+" + t_HEXNUM = r"0x%s+" % hexchar + t_LSHIFT = r"<<" + t_RSHIFT = r">>" + + literals = '"(){}[]<>,;:=|+-*' + + t_ignore = " \t" + + def t_multilinecomment(self, t): + r"/\*(\n|.)*?\*/" + t.lexer.lineno += t.value.count("\n") + if t.value.startswith("/**"): + self._doccomments.append(t.value) + + def t_singlelinecomment(self, t): + r"//[^\n]*" + + def t_IID(self, t): + return t + + t_IID.__doc__ = r"%(c)s{8}-%(c)s{4}-%(c)s{4}-%(c)s{4}-%(c)s{12}" % {"c": hexchar} + + def t_IDENTIFIER(self, t): + r"(unsigned\ long\ long|unsigned\ short|unsigned\ long|long\ long)(?!_?[A-Za-z][A-Za-z_0-9])|_?[A-Za-z][A-Za-z_0-9]*" # NOQA: E501 + t.type = self.keywords.get(t.value, "IDENTIFIER") + return t + + def t_LCDATA(self, t): + r"%\{[ ]*C\+\+[ ]*\n(?P<cdata>(\n|.)*?\n?)%\}[ ]*(C\+\+)?" + t.type = "CDATA" + t.value = t.lexer.lexmatch.group("cdata") + t.lexer.lineno += t.value.count("\n") + return t + + def t_INCLUDE(self, t): + r'\#include[ \t]+"[^"\n]+"' + inc, value, end = t.value.split('"') + t.value = value + return t + + def t_directive(self, t): + r"\#(?P<directive>[a-zA-Z]+)[^\n]+" + raise IDLError( + "Unrecognized directive %s" % t.lexer.lexmatch.group("directive"), + Location( + lexer=self.lexer, lineno=self.lexer.lineno, lexpos=self.lexer.lexpos + ), + ) + + def t_newline(self, t): + r"\n+" + t.lexer.lineno += len(t.value) + + def t_nativeid_NATIVEID(self, t): + # Matches non-parenthesis characters, or a single open and closing + # parenthesis with at least one non-parenthesis character before, + # between and after them (for compatibility with std::function). + r"[^()\n]+(?:\([^()\n]+\)[^()\n]+)?(?=\))" + t.lexer.begin("INITIAL") + return t + + t_nativeid_ignore = "" + + def t_ANY_error(self, t): + raise IDLError( + "unrecognized input", + Location( + lexer=self.lexer, lineno=self.lexer.lineno, lexpos=self.lexer.lexpos + ), + ) + + precedence = ( + ("left", "|"), + ("left", "LSHIFT", "RSHIFT"), + ("left", "+", "-"), + ("left", "*"), + ("left", "UMINUS"), + ) + + def p_idlfile(self, p): + """idlfile : productions""" + p[0] = IDL(p[1]) + + def p_productions_start(self, p): + """productions :""" + p[0] = [] + + def p_productions_cdata(self, p): + """productions : CDATA productions""" + p[0] = list(p[2]) + p[0].insert(0, CDATA(p[1], self.getLocation(p, 1))) + + def p_productions_include(self, p): + """productions : INCLUDE productions""" + p[0] = list(p[2]) + p[0].insert(0, Include(p[1], self.getLocation(p, 1))) + + def p_productions_interface(self, p): + """productions : interface productions + | typedef productions + | native productions + | webidl productions""" + p[0] = list(p[2]) + p[0].insert(0, p[1]) + + def p_typedef(self, p): + """typedef : TYPEDEF type IDENTIFIER ';'""" + p[0] = Typedef( + type=p[2], + name=p[3], + location=self.getLocation(p, 1), + doccomments=p.slice[1].doccomments, + ) + + def p_native(self, p): + """native : attributes NATIVE IDENTIFIER afternativeid '(' NATIVEID ')' ';'""" + p[0] = Native( + name=p[3], + nativename=p[6], + attlist=p[1]["attlist"], + location=self.getLocation(p, 2), + ) + + def p_afternativeid(self, p): + """afternativeid :""" + # this is a place marker: we switch the lexer into literal identifier + # mode here, to slurp up everything until the closeparen + self.lexer.begin("nativeid") + + def p_webidl(self, p): + """webidl : WEBIDL IDENTIFIER ';'""" + p[0] = WebIDL(name=p[2], location=self.getLocation(p, 2)) + + def p_anyident(self, p): + """anyident : IDENTIFIER + | CONST""" + p[0] = {"value": p[1], "location": self.getLocation(p, 1)} + + def p_attributes(self, p): + """attributes : '[' attlist ']' + |""" + if len(p) == 1: + p[0] = {"attlist": []} + else: + p[0] = {"attlist": p[2], "doccomments": p.slice[1].doccomments} + + def p_attlist_start(self, p): + """attlist : attribute""" + p[0] = [p[1]] + + def p_attlist_continue(self, p): + """attlist : attribute ',' attlist""" + p[0] = list(p[3]) + p[0].insert(0, p[1]) + + def p_attribute(self, p): + """attribute : anyident attributeval""" + p[0] = (p[1]["value"], p[2], p[1]["location"]) + + def p_attributeval(self, p): + """attributeval : '(' IDENTIFIER ')' + | '(' IID ')' + |""" + if len(p) > 1: + p[0] = p[2] + + def p_interface(self, p): + """interface : attributes INTERFACE IDENTIFIER ifacebase ifacebody ';'""" + atts, INTERFACE, name, base, body, SEMI = p[1:] + attlist = atts["attlist"] + doccomments = [] + if "doccomments" in atts: + doccomments.extend(atts["doccomments"]) + doccomments.extend(p.slice[2].doccomments) + + def loc(): + return self.getLocation(p, 2) + + if body is None: + # forward-declared interface... must not have attributes! + if len(attlist) != 0: + raise IDLError( + "Forward-declared interface must not have attributes", loc() + ) + + if base is not None: + raise IDLError("Forward-declared interface must not have a base", loc()) + p[0] = Forward(name=name, location=loc(), doccomments=doccomments) + else: + p[0] = Interface( + name=name, + attlist=attlist, + base=base, + members=body, + location=loc(), + doccomments=doccomments, + ) + + def p_ifacebody(self, p): + """ifacebody : '{' members '}' + |""" + if len(p) > 1: + p[0] = p[2] + + def p_ifacebase(self, p): + """ifacebase : ':' IDENTIFIER + |""" + if len(p) == 3: + p[0] = p[2] + + def p_members_start(self, p): + """members :""" + p[0] = [] + + def p_members_continue(self, p): + """members : member members""" + p[0] = list(p[2]) + p[0].insert(0, p[1]) + + def p_member_cdata(self, p): + """member : CDATA""" + p[0] = CDATA(p[1], self.getLocation(p, 1)) + + def p_member_const(self, p): + """member : CONST type IDENTIFIER '=' number ';'""" + p[0] = ConstMember( + type=p[2], + name=p[3], + value=p[5], + location=self.getLocation(p, 1), + doccomments=p.slice[1].doccomments, + ) + + # All "number" products return a function(interface) + + def p_number_decimal(self, p): + """number : NUMBER""" + n = int(p[1]) + p[0] = lambda i: n + + def p_number_hex(self, p): + """number : HEXNUM""" + n = int(p[1], 16) + p[0] = lambda i: n + + def p_number_identifier(self, p): + """number : IDENTIFIER""" + id = p[1] + loc = self.getLocation(p, 1) + p[0] = lambda i: i.getConst(id, loc) + + def p_number_paren(self, p): + """number : '(' number ')'""" + p[0] = p[2] + + def p_number_neg(self, p): + """number : '-' number %prec UMINUS""" + n = p[2] + p[0] = lambda i: -n(i) + + def p_number_add(self, p): + """number : number '+' number + | number '-' number + | number '*' number""" + n1 = p[1] + n2 = p[3] + if p[2] == "+": + p[0] = lambda i: n1(i) + n2(i) + elif p[2] == "-": + p[0] = lambda i: n1(i) - n2(i) + else: + p[0] = lambda i: n1(i) * n2(i) + + def p_number_shift(self, p): + """number : number LSHIFT number + | number RSHIFT number""" + n1 = p[1] + n2 = p[3] + if p[2] == "<<": + p[0] = lambda i: n1(i) << n2(i) + else: + p[0] = lambda i: n1(i) >> n2(i) + + def p_number_bitor(self, p): + """number : number '|' number""" + n1 = p[1] + n2 = p[3] + p[0] = lambda i: n1(i) | n2(i) + + def p_member_cenum(self, p): + """member : CENUM IDENTIFIER ':' NUMBER '{' variants '}' ';'""" + p[0] = CEnum( + name=p[2], + width=int(p[4]), + variants=p[6], + location=self.getLocation(p, 1), + doccomments=p.slice[1].doccomments, + ) + + def p_variants_start(self, p): + """variants :""" + p[0] = [] + + def p_variants_single(self, p): + """variants : variant""" + p[0] = [p[1]] + + def p_variants_continue(self, p): + """variants : variant ',' variants""" + p[0] = [p[1]] + p[3] + + def p_variant_implicit(self, p): + """variant : IDENTIFIER""" + p[0] = CEnumVariant(p[1], None, self.getLocation(p, 1)) + + def p_variant_explicit(self, p): + """variant : IDENTIFIER '=' number""" + p[0] = CEnumVariant(p[1], p[3], self.getLocation(p, 1)) + + def p_member_att(self, p): + """member : attributes optreadonly ATTRIBUTE type IDENTIFIER ';'""" + if "doccomments" in p[1]: + doccomments = p[1]["doccomments"] + elif p[2] is not None: + doccomments = p[2] + else: + doccomments = p.slice[3].doccomments + + p[0] = Attribute( + type=p[4], + name=p[5], + attlist=p[1]["attlist"], + readonly=p[2] is not None, + location=self.getLocation(p, 3), + doccomments=doccomments, + ) + + def p_member_method(self, p): + """member : attributes type IDENTIFIER '(' paramlist ')' raises ';'""" + if "doccomments" in p[1]: + doccomments = p[1]["doccomments"] + else: + doccomments = p.slice[2].doccomments + + p[0] = Method( + type=p[2], + name=p[3], + attlist=p[1]["attlist"], + paramlist=p[5], + location=self.getLocation(p, 3), + doccomments=doccomments, + raises=p[7], + ) + + def p_paramlist(self, p): + """paramlist : param moreparams + |""" + if len(p) == 1: + p[0] = [] + else: + p[0] = list(p[2]) + p[0].insert(0, p[1]) + + def p_moreparams_start(self, p): + """moreparams :""" + p[0] = [] + + def p_moreparams_continue(self, p): + """moreparams : ',' param moreparams""" + p[0] = list(p[3]) + p[0].insert(0, p[2]) + + def p_param(self, p): + """param : attributes paramtype type IDENTIFIER""" + p[0] = Param( + paramtype=p[2], + type=p[3], + name=p[4], + attlist=p[1]["attlist"], + location=self.getLocation(p, 4), + ) + + def p_paramtype(self, p): + """paramtype : IN + | INOUT + | OUT""" + p[0] = p[1] + + def p_optreadonly(self, p): + """optreadonly : READONLY + |""" + if len(p) > 1: + p[0] = p.slice[1].doccomments + else: + p[0] = None + + def p_raises(self, p): + """raises : RAISES '(' idlist ')' + |""" + if len(p) == 1: + p[0] = [] + else: + p[0] = p[3] + + def p_idlist(self, p): + """idlist : IDENTIFIER""" + p[0] = [p[1]] + + def p_idlist_continue(self, p): + """idlist : IDENTIFIER ',' idlist""" + p[0] = list(p[3]) + p[0].insert(0, p[1]) + + def p_type_id(self, p): + """type : IDENTIFIER""" + p[0] = TypeId(name=p[1]) + p.slice[0].doccomments = p.slice[1].doccomments + + def p_type_generic(self, p): + """type : IDENTIFIER '<' typelist '>'""" + p[0] = TypeId(name=p[1], params=p[3]) + p.slice[0].doccomments = p.slice[1].doccomments + + def p_typelist(self, p): + """typelist : type""" + p[0] = [p[1]] + + def p_typelist_continue(self, p): + """typelist : type ',' typelist""" + p[0] = list(p[3]) + p[0].insert(0, p[1]) + + def p_error(self, t): + if not t: + raise IDLError( + "Syntax Error at end of file. Possibly due to missing semicolon(;), braces(}) " + "or both", + None, + ) + else: + location = Location(self.lexer, t.lineno, t.lexpos) + raise IDLError("invalid syntax", location) + + def __init__(self): + self._doccomments = [] + self.lexer = lex.lex(object=self, debug=False) + self.parser = yacc.yacc(module=self, write_tables=False, debug=False) + + def clearComments(self): + self._doccomments = [] + + def token(self): + t = self.lexer.token() + if t is not None and t.type != "CDATA": + t.doccomments = self._doccomments + self._doccomments = [] + return t + + def parse(self, data, filename=None): + if filename is not None: + self.lexer.filename = filename + self.lexer.lineno = 1 + self.lexer.input(data) + idl = self.parser.parse(lexer=self) + if filename is not None: + idl.deps.append(filename) + return idl + + def getLocation(self, p, i): + return Location(self.lexer, p.lineno(i), p.lexpos(i)) + + +if __name__ == "__main__": + p = IDLParser() + for f in sys.argv[1:]: + print("Parsing %s" % f) + p.parse(open(f, encoding="utf-8").read(), filename=f) |