1#!/usr/bin/env python
2# jsonlink.py - Merge JSON typelib files into a .cpp file
3#
4# This Source Code Form is subject to the terms of the Mozilla Public
5# License, v. 2.0. If a copy of the MPL was not distributed with this
6# file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
8import json
9from perfecthash import PerfectHash
10from collections import OrderedDict
11import buildconfig
12
13# Pick a nice power-of-two size for our intermediate PHF tables.
14PHFSIZE = 512
15
16
17def indented(s):
18    return s.replace("\n", "\n  ")
19
20
21def cpp(v):
22    if type(v) == bool:
23        return "true" if v else "false"
24    return str(v)
25
26
27def mkstruct(*fields):
28    def mk(comment, **vals):
29        assert len(fields) == len(vals)
30        r = "{ // " + comment
31        r += indented(",".join("\n/* %s */ %s" % (k, cpp(vals[k])) for k in fields))
32        r += "\n}"
33        return r
34
35    return mk
36
37
38##########################################################
39# Ensure these fields are in the same order as xptinfo.h #
40##########################################################
41nsXPTInterfaceInfo = mkstruct(
42    "mIID",
43    "mName",
44    "mParent",
45    "mBuiltinClass",
46    "mMainProcessScriptableOnly",
47    "mMethods",
48    "mConsts",
49    "mFunction",
50    "mNumMethods",
51    "mNumConsts",
52)
53
54##########################################################
55# Ensure these fields are in the same order as xptinfo.h #
56##########################################################
57nsXPTType = mkstruct(
58    "mTag",
59    "mInParam",
60    "mOutParam",
61    "mOptionalParam",
62    "mData1",
63    "mData2",
64)
65
66##########################################################
67# Ensure these fields are in the same order as xptinfo.h #
68##########################################################
69nsXPTParamInfo = mkstruct(
70    "mType",
71)
72
73##########################################################
74# Ensure these fields are in the same order as xptinfo.h #
75##########################################################
76nsXPTMethodInfo = mkstruct(
77    "mName",
78    "mParams",
79    "mNumParams",
80    "mGetter",
81    "mSetter",
82    "mReflectable",
83    "mOptArgc",
84    "mContext",
85    "mHasRetval",
86    "mIsSymbol",
87)
88
89##########################################################
90# Ensure these fields are in the same order as xptinfo.h #
91##########################################################
92nsXPTDOMObjectInfo = mkstruct(
93    "mUnwrap",
94    "mWrap",
95    "mCleanup",
96)
97
98##########################################################
99# Ensure these fields are in the same order as xptinfo.h #
100##########################################################
101nsXPTConstantInfo = mkstruct(
102    "mName",
103    "mSigned",
104    "mValue",
105)
106
107
108# Helper functions for dealing with IIDs.
109#
110# Unfortunately, the way we represent IIDs in memory depends on the endianness
111# of the target architecture. We store an nsIID as a 16-byte, 4-tuple of:
112#
113#   (uint32_t, uint16_t, uint16_t, [uint8_t; 8])
114#
115# Unfortunately, this means that when we hash the bytes of the nsIID on a
116# little-endian target system, we need to hash them in little-endian order.
117# These functions let us split the input hexadecimal string into components,
118# encoding each as a little-endian value, and producing an accurate bytearray.
119#
120# It would be nice to have a consistent representation of IIDs in memory such
121# that we don't have to do these gymnastics to get an accurate hash.
122
123
124def split_at_idxs(s, lengths):
125    idx = 0
126    for length in lengths:
127        yield s[idx : idx + length]
128        idx += length
129    assert idx == len(s)
130
131
132def split_iid(iid):  # Get the individual components out of an IID string.
133    iid = iid.replace("-", "")  # Strip any '-' delimiters
134    return tuple(split_at_idxs(iid, (8, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2)))
135
136
137def iid_bytes(iid):  # Get the byte representation of the IID for hashing.
138    bs = bytearray()
139    for num in split_iid(iid):
140        b = bytearray.fromhex(num)
141        # Match endianness of the target platform for each component
142        if buildconfig.substs["TARGET_ENDIANNESS"] == "little":
143            b.reverse()
144        bs += b
145    return bs
146
147
148# Split a 16-bit integer into its high and low 8 bits
149def splitint(i):
150    assert i < 2 ** 16
151    return (i >> 8, i & 0xFF)
152
153
154# Occasionally in xpconnect, we need to fabricate types to pass into the
155# conversion methods. In some cases, these types need to be arrays, which hold
156# indicies into the extra types array.
157#
158# These are some types which should have known indexes into the extra types
159# array.
160utility_types = [
161    {"tag": "TD_INT8"},
162    {"tag": "TD_UINT8"},
163    {"tag": "TD_INT16"},
164    {"tag": "TD_UINT16"},
165    {"tag": "TD_INT32"},
166    {"tag": "TD_UINT32"},
167    {"tag": "TD_INT64"},
168    {"tag": "TD_UINT64"},
169    {"tag": "TD_FLOAT"},
170    {"tag": "TD_DOUBLE"},
171    {"tag": "TD_BOOL"},
172    {"tag": "TD_CHAR"},
173    {"tag": "TD_WCHAR"},
174    {"tag": "TD_NSIDPTR"},
175    {"tag": "TD_PSTRING"},
176    {"tag": "TD_PWSTRING"},
177    {"tag": "TD_INTERFACE_IS_TYPE", "iid_is": 0},
178]
179
180
181# Core of the code generator. Takes a list of raw JSON XPT interfaces, and
182# writes out a file containing the necessary static declarations into fd.
183def link_to_cpp(interfaces, fd, header_fd):
184    # Perfect Hash from IID to interface.
185    iid_phf = PerfectHash(interfaces, PHFSIZE, key=lambda i: iid_bytes(i["uuid"]))
186    for idx, iface in enumerate(iid_phf.entries):
187        iface["idx"] = idx  # Store the index in iid_phf of the entry.
188
189    # Perfect Hash from name to iid_phf index.
190    name_phf = PerfectHash(interfaces, PHFSIZE, key=lambda i: i["name"].encode("ascii"))
191
192    def interface_idx(name):
193        entry = name and name_phf.get_entry(name.encode("ascii"))
194        if entry:
195            return entry["idx"] + 1  # 1-based, use 0 as a sentinel.
196        return 0
197
198    # NOTE: State used while linking. This is done with closures rather than a
199    # class due to how this file's code evolved.
200    includes = set()
201    types = []
202    type_cache = {}
203    params = []
204    param_cache = {}
205    methods = []
206    max_params = 0
207    method_with_max_params = None
208    consts = []
209    domobjects = []
210    domobject_cache = {}
211    strings = OrderedDict()
212
213    def lower_uuid(uuid):
214        return (
215            "{0x%s, 0x%s, 0x%s, {0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s}}"
216            % split_iid(uuid)
217        )
218
219    def lower_domobject(do):
220        assert do["tag"] == "TD_DOMOBJECT"
221
222        idx = domobject_cache.get(do["name"])
223        if idx is None:
224            idx = domobject_cache[do["name"]] = len(domobjects)
225
226            includes.add(do["headerFile"])
227            domobjects.append(
228                nsXPTDOMObjectInfo(
229                    "%d = %s" % (idx, do["name"]),
230                    # These methods are defined at the top of the generated file.
231                    mUnwrap="UnwrapDOMObject<mozilla::dom::prototypes::id::%s, %s>"
232                    % (do["name"], do["native"]),
233                    mWrap="WrapDOMObject<%s>" % do["native"],
234                    mCleanup="CleanupDOMObject<%s>" % do["native"],
235                )
236            )
237
238        return idx
239
240    def lower_string(s):
241        if s in strings:
242            # We've already seen this string.
243            return strings[s]
244        elif len(strings):
245            # Get the last string we inserted (should be O(1) on OrderedDict).
246            last_s = next(reversed(strings))
247            strings[s] = strings[last_s] + len(last_s) + 1
248        else:
249            strings[s] = 0
250        return strings[s]
251
252    def lower_symbol(s):
253        return "uint32_t(JS::SymbolCode::%s)" % s
254
255    def lower_extra_type(type):
256        key = describe_type(type)
257        idx = type_cache.get(key)
258        if idx is None:
259            idx = type_cache[key] = len(types)
260            # Make sure `types` is the proper length for any recursive calls
261            # to `lower_extra_type` that might happen from within `lower_type`.
262            types.append(None)
263            realtype = lower_type(type)
264            types[idx] = realtype
265        return idx
266
267    def describe_type(type):  # Create the type's documentation comment.
268        tag = type["tag"][3:].lower()
269        if tag == "legacy_array":
270            return "%s[size_is=%d]" % (describe_type(type["element"]), type["size_is"])
271        elif tag == "array":
272            return "Array<%s>" % describe_type(type["element"])
273        elif tag == "interface_type" or tag == "domobject":
274            return type["name"]
275        elif tag == "interface_is_type":
276            return "iid_is(%d)" % type["iid_is"]
277        elif tag.endswith("_size_is"):
278            return "%s(size_is=%d)" % (tag, type["size_is"])
279        return tag
280
281    def lower_type(type, in_=False, out=False, optional=False):
282        tag = type["tag"]
283        d1 = d2 = 0
284
285        # TD_VOID is used for types that can't be represented in JS, so they
286        # should not be represented in the XPT info.
287        assert tag != "TD_VOID"
288
289        if tag == "TD_LEGACY_ARRAY":
290            d1 = type["size_is"]
291            d2 = lower_extra_type(type["element"])
292
293        elif tag == "TD_ARRAY":
294            # NOTE: TD_ARRAY can hold 16 bits of type index, while
295            # TD_LEGACY_ARRAY can only hold 8.
296            d1, d2 = splitint(lower_extra_type(type["element"]))
297
298        elif tag == "TD_INTERFACE_TYPE":
299            d1, d2 = splitint(interface_idx(type["name"]))
300
301        elif tag == "TD_INTERFACE_IS_TYPE":
302            d1 = type["iid_is"]
303
304        elif tag == "TD_DOMOBJECT":
305            d1, d2 = splitint(lower_domobject(type))
306
307        elif tag.endswith("_SIZE_IS"):
308            d1 = type["size_is"]
309
310        assert d1 < 256 and d2 < 256, "Data values too large"
311        return nsXPTType(
312            describe_type(type),
313            mTag=tag,
314            mData1=d1,
315            mData2=d2,
316            mInParam=in_,
317            mOutParam=out,
318            mOptionalParam=optional,
319        )
320
321    def lower_param(param, paramname):
322        params.append(
323            nsXPTParamInfo(
324                "%d = %s" % (len(params), paramname),
325                mType=lower_type(
326                    param["type"],
327                    in_="in" in param["flags"],
328                    out="out" in param["flags"],
329                    optional="optional" in param["flags"],
330                ),
331            )
332        )
333
334    def is_method_reflectable(method):
335        if "hidden" in method["flags"]:
336            return False
337
338        for param in method["params"]:
339            # Reflected methods can't use native types. All native types end up
340            # getting tagged as void*, so this check is easy.
341            if param["type"]["tag"] == "TD_VOID":
342                return False
343
344        return True
345
346    def lower_method(method, ifacename):
347        methodname = "%s::%s" % (ifacename, method["name"])
348
349        isSymbol = "symbol" in method["flags"]
350        reflectable = is_method_reflectable(method)
351
352        if not reflectable:
353            # Hide the parameters of methods that can't be called from JS to
354            # reduce the size of the file.
355            paramidx = name = numparams = 0
356        else:
357            if isSymbol:
358                name = lower_symbol(method["name"])
359            else:
360                name = lower_string(method["name"])
361
362            numparams = len(method["params"])
363
364            # Check cache for parameters
365            cachekey = json.dumps(method["params"], sort_keys=True)
366            paramidx = param_cache.get(cachekey)
367            if paramidx is None:
368                paramidx = param_cache[cachekey] = len(params)
369                for idx, param in enumerate(method["params"]):
370                    lower_param(param, "%s[%d]" % (methodname, idx))
371
372        nonlocal max_params, method_with_max_params
373        if numparams > max_params:
374            max_params = numparams
375            method_with_max_params = methodname
376        methods.append(
377            nsXPTMethodInfo(
378                "%d = %s" % (len(methods), methodname),
379                mName=name,
380                mParams=paramidx,
381                mNumParams=numparams,
382                # Flags
383                mGetter="getter" in method["flags"],
384                mSetter="setter" in method["flags"],
385                mReflectable=reflectable,
386                mOptArgc="optargc" in method["flags"],
387                mContext="jscontext" in method["flags"],
388                mHasRetval="hasretval" in method["flags"],
389                mIsSymbol=isSymbol,
390            )
391        )
392
393    def lower_const(const, ifacename):
394        assert const["type"]["tag"] in [
395            "TD_INT16",
396            "TD_INT32",
397            "TD_UINT8",
398            "TD_UINT16",
399            "TD_UINT32",
400        ]
401        is_signed = const["type"]["tag"] in ["TD_INT16", "TD_INT32"]
402
403        # Constants are always either signed or unsigned 16 or 32 bit integers,
404        # which we will only need to convert to JS values. To save on space,
405        # don't bother storing the type, and instead just store a 32-bit
406        # unsigned integer, and stash whether to interpret it as signed.
407        consts.append(
408            nsXPTConstantInfo(
409                "%d = %s::%s" % (len(consts), ifacename, const["name"]),
410                mName=lower_string(const["name"]),
411                mSigned=is_signed,
412                mValue="(uint32_t)%d" % const["value"],
413            )
414        )
415
416    def ancestors(iface):
417        yield iface
418        while iface["parent"]:
419            iface = name_phf.get_entry(iface["parent"].encode("ascii"))
420            yield iface
421
422    def lower_iface(iface):
423        method_cnt = sum(len(i["methods"]) for i in ancestors(iface))
424        const_cnt = sum(len(i["consts"]) for i in ancestors(iface))
425
426        # The number of maximum methods is not arbitrary. It is the same value
427        # as in xpcom/reflect/xptcall/genstubs.pl; do not change this value
428        # without changing that one or you WILL see problems.
429        #
430        # In addition, mNumMethods and mNumConsts are stored as a 8-bit ints,
431        # meaning we cannot exceed 255 methods/consts on any interface.
432        assert method_cnt < 250, "%s has too many methods" % iface["name"]
433        assert const_cnt < 256, "%s has too many constants" % iface["name"]
434
435        # Store the lowered interface as 'cxx' on the iface object.
436        iface["cxx"] = nsXPTInterfaceInfo(
437            "%d = %s" % (iface["idx"], iface["name"]),
438            mIID=lower_uuid(iface["uuid"]),
439            mName=lower_string(iface["name"]),
440            mParent=interface_idx(iface["parent"]),
441            mMethods=len(methods),
442            mNumMethods=method_cnt,
443            mConsts=len(consts),
444            mNumConsts=const_cnt,
445            # Flags
446            mBuiltinClass="builtinclass" in iface["flags"],
447            mMainProcessScriptableOnly="main_process_only" in iface["flags"],
448            mFunction="function" in iface["flags"],
449        )
450
451        # Lower methods and constants used by this interface
452        for method in iface["methods"]:
453            lower_method(method, iface["name"])
454        for const in iface["consts"]:
455            lower_const(const, iface["name"])
456
457    # Lower the types which have fixed indexes first, and check that the indexes
458    # seem correct.
459    for expected, ty in enumerate(utility_types):
460        got = lower_extra_type(ty)
461        assert got == expected, "Wrong index when lowering"
462
463    # Lower interfaces in the order of the IID phf's entries lookup.
464    for iface in iid_phf.entries:
465        lower_iface(iface)
466
467    # Write out the final output files
468    fd.write("/* THIS FILE WAS GENERATED BY xptcodegen.py - DO NOT EDIT */\n\n")
469    header_fd.write("/* THIS FILE WAS GENERATED BY xptcodegen.py - DO NOT EDIT */\n\n")
470
471    header_fd.write(
472        """
473#ifndef xptdata_h
474#define xptdata_h
475
476enum class nsXPTInterface : uint16_t {
477"""
478    )
479
480    for entry in iid_phf.entries:
481        header_fd.write("  %s,\n" % entry["name"])
482
483    header_fd.write(
484        """
485};
486
487#endif
488"""
489    )
490
491    # Include any bindings files which we need to include for webidl types
492    for include in sorted(includes):
493        fd.write('#include "%s"\n' % include)
494
495    # Write out our header
496    fd.write(
497        """
498#include "xptinfo.h"
499#include "mozilla/PerfectHash.h"
500#include "mozilla/dom/BindingUtils.h"
501
502// These template methods are specialized to be used in the sDOMObjects table.
503template<mozilla::dom::prototypes::ID PrototypeID, typename T>
504static nsresult UnwrapDOMObject(JS::HandleValue aHandle, void** aObj, JSContext* aCx)
505{
506  RefPtr<T> p;
507  nsresult rv = mozilla::dom::UnwrapObject<PrototypeID, T>(aHandle, p, aCx);
508  p.forget(aObj);
509  return rv;
510}
511
512template<typename T>
513static bool WrapDOMObject(JSContext* aCx, void* aObj, JS::MutableHandleValue aHandle)
514{
515  return mozilla::dom::GetOrCreateDOMReflector(aCx, reinterpret_cast<T*>(aObj), aHandle);
516}
517
518template<typename T>
519static void CleanupDOMObject(void* aObj)
520{
521  RefPtr<T> p = already_AddRefed<T>(reinterpret_cast<T*>(aObj));
522}
523
524namespace xpt {
525namespace detail {
526
527"""
528    )
529
530    # Static data arrays
531    def array(ty, name, els):
532        fd.write(
533            "const %s %s[] = {%s\n};\n\n"
534            % (ty, name, ",".join(indented("\n" + str(e)) for e in els))
535        )
536
537    array("nsXPTType", "sTypes", types)
538    array("nsXPTParamInfo", "sParams", params)
539    array("nsXPTMethodInfo", "sMethods", methods)
540    # Verify that stack-allocated buffers will do for xptcall implementations.
541    msg = (
542        "Too many method arguments in %s. "
543        "Either reduce the number of arguments "
544        "or increase PARAM_BUFFER_COUNT." % method_with_max_params
545    )
546    fd.write('static_assert(%s <= PARAM_BUFFER_COUNT, "%s");\n\n' % (max_params, msg))
547    array("nsXPTDOMObjectInfo", "sDOMObjects", domobjects)
548    array("nsXPTConstantInfo", "sConsts", consts)
549
550    # The strings array. We write out individual characters to avoid MSVC restrictions.
551    fd.write("const char sStrings[] = {\n")
552    for s, off in strings.items():
553        fd.write("  // %d = %s\n  '%s','\\0',\n" % (off, s, "','".join(s)))
554    fd.write("};\n\n")
555
556    # Build the perfect hash table for InterfaceByIID
557    fd.write(
558        iid_phf.cxx_codegen(
559            name="InterfaceByIID",
560            entry_type="nsXPTInterfaceInfo",
561            entries_name="sInterfaces",
562            lower_entry=lambda iface: iface["cxx"],
563            # Check that the IIDs match to support IID keys not in the map.
564            return_type="const nsXPTInterfaceInfo*",
565            return_entry="return entry.IID().Equals(aKey) ? &entry : nullptr;",
566            key_type="const nsIID&",
567            key_bytes="reinterpret_cast<const char*>(&aKey)",
568            key_length="sizeof(nsIID)",
569        )
570    )
571    fd.write("\n")
572
573    # Build the perfect hash table for InterfaceByName
574    fd.write(
575        name_phf.cxx_codegen(
576            name="InterfaceByName",
577            entry_type="uint16_t",
578            lower_entry=lambda iface: "%-4d /* %s */" % (iface["idx"], iface["name"]),
579            # Get the actual nsXPTInterfaceInfo from sInterfaces, and
580            # double-check that names match.
581            return_type="const nsXPTInterfaceInfo*",
582            return_entry="return strcmp(sInterfaces[entry].Name(), aKey) == 0"
583            " ? &sInterfaces[entry] : nullptr;",
584        )
585    )
586    fd.write("\n")
587
588    # Generate some checks that the indexes for the utility types match the
589    # declared ones in xptinfo.h
590    for idx, ty in enumerate(utility_types):
591        fd.write(
592            'static_assert(%d == (uint8_t)nsXPTType::Idx::%s, "Bad idx");\n'
593            % (idx, ty["tag"][3:])
594        )
595
596    fd.write(
597        """
598const uint16_t sInterfacesSize = mozilla::ArrayLength(sInterfaces);
599
600} // namespace detail
601} // namespace xpt
602"""
603    )
604
605
606def link_and_write(files, outfile, outheader):
607    interfaces = []
608    for file in files:
609        with open(file, "r") as fd:
610            interfaces += json.load(fd)
611
612    iids = set()
613    names = set()
614    for interface in interfaces:
615        assert interface["uuid"] not in iids, "duplicated UUID %s" % interface["uuid"]
616        assert interface["name"] not in names, "duplicated name %s" % interface["name"]
617        iids.add(interface["uuid"])
618        names.add(interface["name"])
619
620    link_to_cpp(interfaces, outfile, outheader)
621
622
623def main():
624    from argparse import ArgumentParser
625    import sys
626
627    parser = ArgumentParser()
628    parser.add_argument("outfile", help="Output C++ file to generate")
629    parser.add_argument("outheader", help="Output C++ header file to generate")
630    parser.add_argument("xpts", nargs="*", help="source xpt files")
631
632    args = parser.parse_args(sys.argv[1:])
633    with open(args.outfile, "w") as fd, open(args.outheader, "w") as header_fd:
634        link_and_write(args.xpts, fd, header_fd)
635
636
637if __name__ == "__main__":
638    main()
639