xpcom/reflect/xptinfo/xptcodegen.py
author Kyle Machulis <kyle@nonpolynomial.com>
Tue, 06 Nov 2018 00:05:35 +0000
changeset 444500 edfa81173d9d596888b23e136d6357cd7d4e0097
parent 442911 fbb568aa121e33fedf00c3be48a04c1593af9591
child 456444 ca65b46b0d3708a115042dd4f5484bdc7a269a6b
permissions -rw-r--r--
Bug 1498059 - Allow 8-bit unsigned consts in XPIDL; , r=nika,froydnj Differential Revision: https://phabricator.services.mozilla.com/D8593

#!/usr/bin/env python
# jsonlink.py - Merge JSON typelib files into a .cpp file
#
# 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/.

import json
from perfecthash import PerfectHash
from collections import OrderedDict
import buildconfig

# Pick a nice power-of-two size for our intermediate PHF tables.
PHFSIZE = 512


def indented(s):
    return s.replace('\n', '\n  ')


def cpp(v):
    if type(v) == bool:
        return "true" if v else "false"
    return str(v)


def mkstruct(*fields):
    def mk(comment, **vals):
        assert len(fields) == len(vals)
        r = "{ // " + comment
        r += indented(','.join(
            "\n/* %s */ %s" % (k, cpp(vals[k])) for k in fields))
        r += "\n}"
        return r
    return mk


##########################################################
# Ensure these fields are in the same order as xptinfo.h #
##########################################################
nsXPTInterfaceInfo = mkstruct(
    "mIID",
    "mName",
    "mParent",
    "mBuiltinClass",
    "mMainProcessScriptableOnly",
    "mMethods",
    "mConsts",
    "mFunction",
    "mNumMethods",
    "mNumConsts",
)

##########################################################
# Ensure these fields are in the same order as xptinfo.h #
##########################################################
nsXPTType = mkstruct(
    "mTag",
    "mInParam",
    "mOutParam",
    "mOptionalParam",
    "mData1",
    "mData2",
)

##########################################################
# Ensure these fields are in the same order as xptinfo.h #
##########################################################
nsXPTParamInfo = mkstruct(
    "mType",
)

##########################################################
# Ensure these fields are in the same order as xptinfo.h #
##########################################################
nsXPTMethodInfo = mkstruct(
    "mName",
    "mParams",
    "mNumParams",
    "mGetter",
    "mSetter",
    "mNotXPCOM",
    "mHidden",
    "mOptArgc",
    "mContext",
    "mHasRetval",
    "mIsSymbol",
)

##########################################################
# Ensure these fields are in the same order as xptinfo.h #
##########################################################
nsXPTDOMObjectInfo = mkstruct(
    "mUnwrap",
    "mWrap",
    "mCleanup",
)

##########################################################
# Ensure these fields are in the same order as xptinfo.h #
##########################################################
nsXPTConstantInfo = mkstruct(
    "mName",
    "mSigned",
    "mValue",
)


# Helper functions for dealing with IIDs.
#
# Unfortunately, the way we represent IIDs in memory depends on the endianness
# of the target architecture. We store an nsIID as a 16-byte, 4-tuple of:
#
#   (uint32_t, uint16_t, uint16_t, [uint8_t; 8])
#
# Unfortunately, this means that when we hash the bytes of the nsIID on a
# little-endian target system, we need to hash them in little-endian order.
# These functions let us split the input hexadecimal string into components,
# encoding each as a little-endian value, and producing an accurate bytearray.
#
# It would be nice to have a consistent representation of IIDs in memory such
# that we don't have to do these gymnastics to get an accurate hash.

def split_at_idxs(s, lengths):
    idx = 0
    for length in lengths:
        yield s[idx:idx+length]
        idx += length
    assert idx == len(s)


def split_iid(iid):  # Get the individual components out of an IID string.
    iid = iid.replace('-', '')  # Strip any '-' delimiters
    return tuple(split_at_idxs(iid, (8, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2)))


def iid_bytes(iid):  # Get the byte representation of the IID for hashing.
    bs = bytearray()
    for num in split_iid(iid):
        b = bytearray.fromhex(num)
        # Match endianness of the target platform for each component
        if buildconfig.substs['TARGET_ENDIANNESS'] == 'little':
            b.reverse()
        bs += b
    return bs


# Split a 16-bit integer into its high and low 8 bits
def splitint(i):
    assert i < 2**16
    return (i >> 8, i & 0xff)


# Occasionally in xpconnect, we need to fabricate types to pass into the
# conversion methods. In some cases, these types need to be arrays, which hold
# indicies into the extra types array.
#
# These are some types which should have known indexes into the extra types
# array.
utility_types = [
    {'tag': 'TD_INT8'},
    {'tag': 'TD_UINT8'},
    {'tag': 'TD_INT16'},
    {'tag': 'TD_UINT16'},
    {'tag': 'TD_INT32'},
    {'tag': 'TD_UINT32'},
    {'tag': 'TD_INT64'},
    {'tag': 'TD_UINT64'},
    {'tag': 'TD_FLOAT'},
    {'tag': 'TD_DOUBLE'},
    {'tag': 'TD_BOOL'},
    {'tag': 'TD_CHAR'},
    {'tag': 'TD_WCHAR'},
    {'tag': 'TD_PNSIID'},
    {'tag': 'TD_PSTRING'},
    {'tag': 'TD_PWSTRING'},
    {'tag': 'TD_INTERFACE_IS_TYPE', 'iid_is': 0},
]


# Core of the code generator. Takes a list of raw JSON XPT interfaces, and
# writes out a file containing the necessary static declarations into fd.
def link_to_cpp(interfaces, fd):
    # Perfect Hash from IID to interface.
    iid_phf = PerfectHash(interfaces, PHFSIZE,
                          key=lambda i: iid_bytes(i['uuid']))
    for idx, iface in enumerate(iid_phf.entries):
        iface['idx'] = idx  # Store the index in iid_phf of the entry.

    # Perfect Hash from name to iid_phf index.
    name_phf = PerfectHash(interfaces, PHFSIZE,
                           key=lambda i: i['name'].encode('ascii'))

    def interface_idx(name):
        entry = name and name_phf.get_entry(name.encode('ascii'))
        if entry:
            return entry['idx'] + 1  # 1-based, use 0 as a sentinel.
        return 0

    # NOTE: State used while linking. This is done with closures rather than a
    # class due to how this file's code evolved.
    includes = set()
    types = []
    type_cache = {}
    params = []
    param_cache = {}
    methods = []
    consts = []
    domobjects = []
    domobject_cache = {}
    strings = OrderedDict()

    def lower_uuid(uuid):
        return ("{0x%s, 0x%s, 0x%s, {0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s}}" %
                split_iid(uuid))

    def lower_domobject(do):
        assert do['tag'] == 'TD_DOMOBJECT'

        idx = domobject_cache.get(do['name'])
        if idx is None:
            idx = domobject_cache[do['name']] = len(domobjects)

            includes.add(do['headerFile'])
            domobjects.append(nsXPTDOMObjectInfo(
                "%d = %s" % (idx, do['name']),
                # These methods are defined at the top of the generated file.
                mUnwrap="UnwrapDOMObject<mozilla::dom::prototypes::id::%s, %s>" %
                (do['name'], do['native']),
                mWrap="WrapDOMObject<%s>" % do['native'],
                mCleanup="CleanupDOMObject<%s>" % do['native'],
            ))

        return idx

    def lower_string(s):
        if s in strings:
            # We've already seen this string.
            return strings[s]
        elif len(strings):
            # Get the last string we inserted (should be O(1) on OrderedDict).
            last_s = next(reversed(strings))
            strings[s] = strings[last_s] + len(last_s) + 1
        else:
            strings[s] = 0
        return strings[s]

    def lower_symbol(s):
        return "uint32_t(JS::SymbolCode::%s)" % s

    def lower_extra_type(type):
        key = describe_type(type)
        idx = type_cache.get(key)
        if idx is None:
            idx = type_cache[key] = len(types)
            types.append(lower_type(type))
        return idx

    def describe_type(type):  # Create the type's documentation comment.
        tag = type['tag'][3:].lower()
        if tag == 'legacy_array':
            return '%s[size_is=%d]' % (
                describe_type(type['element']), type['size_is'])
        elif tag == 'array':
            return 'Array<%s>' % describe_type(type['element'])
        elif tag == 'interface_type' or tag == 'domobject':
            return type['name']
        elif tag == 'interface_is_type':
            return 'iid_is(%d)' % type['iid_is']
        elif tag.endswith('_size_is'):
            return '%s(size_is=%d)' % (tag, type['size_is'])
        return tag

    def lower_type(type, in_=False, out=False, optional=False):
        tag = type['tag']
        d1 = d2 = 0

        if tag == 'TD_LEGACY_ARRAY':
            d1 = type['size_is']
            d2 = lower_extra_type(type['element'])

        elif tag == 'TD_ARRAY':
            # NOTE: TD_ARRAY can hold 16 bits of type index, while
            # TD_LEGACY_ARRAY can only hold 8.
            d1, d2 = splitint(lower_extra_type(type['element']))

        elif tag == 'TD_INTERFACE_TYPE':
            d1, d2 = splitint(interface_idx(type['name']))

        elif tag == 'TD_INTERFACE_IS_TYPE':
            d1 = type['iid_is']

        elif tag == 'TD_DOMOBJECT':
            d1, d2 = splitint(lower_domobject(type))

        elif tag.endswith('_SIZE_IS'):
            d1 = type['size_is']

        assert d1 < 256 and d2 < 256, "Data values too large"
        return nsXPTType(
            describe_type(type),
            mTag=tag,
            mData1=d1,
            mData2=d2,
            mInParam=in_,
            mOutParam=out,
            mOptionalParam=optional,
        )

    def lower_param(param, paramname):
        params.append(nsXPTParamInfo(
            "%d = %s" % (len(params), paramname),
            mType=lower_type(param['type'],
                             in_='in' in param['flags'],
                             out='out' in param['flags'],
                             optional='optional' in param['flags'])
        ))

    def lower_method(method, ifacename):
        methodname = "%s::%s" % (ifacename, method['name'])

        isSymbol = 'symbol' in method['flags']

        if 'notxpcom' in method['flags'] or 'hidden' in method['flags']:
            paramidx = name = numparams = 0  # hide parameters
        else:
            if isSymbol:
                name = lower_symbol(method['name'])
            else:
                name = lower_string(method['name'])

            numparams = len(method['params'])

            # Check cache for parameters
            cachekey = json.dumps(method['params'])
            paramidx = param_cache.get(cachekey)
            if paramidx is None:
                paramidx = param_cache[cachekey] = len(params)
                for idx, param in enumerate(method['params']):
                    lower_param(param, "%s[%d]" % (methodname, idx))

        methods.append(nsXPTMethodInfo(
            "%d = %s" % (len(methods), methodname),

            # If our method is hidden, we can save some memory by not
            # generating parameter info about it.
            mName=name,
            mParams=paramidx,
            mNumParams=numparams,

            # Flags
            mGetter='getter' in method['flags'],
            mSetter='setter' in method['flags'],
            mNotXPCOM='notxpcom' in method['flags'],
            mHidden='hidden' in method['flags'],
            mOptArgc='optargc' in method['flags'],
            mContext='jscontext' in method['flags'],
            mHasRetval='hasretval' in method['flags'],
            mIsSymbol=isSymbol,
        ))

    def lower_const(const, ifacename):
        assert const['type']['tag'] in \
            ['TD_INT16', 'TD_INT32', 'TD_UINT8', 'TD_UINT16', 'TD_UINT32']
        is_signed = const['type']['tag'] in ['TD_INT16', 'TD_INT32']

        # Constants are always either signed or unsigned 16 or 32 bit integers,
        # which we will only need to convert to JS values. To save on space,
        # don't bother storing the type, and instead just store a 32-bit
        # unsigned integer, and stash whether to interpret it as signed.
        consts.append(nsXPTConstantInfo(
            "%d = %s::%s" % (len(consts), ifacename, const['name']),

            mName=lower_string(const['name']),
            mSigned=is_signed,
            mValue="(uint32_t)%d" % const['value'],
        ))

    def ancestors(iface):
        yield iface
        while iface['parent']:
            iface = name_phf.get_entry(iface['parent'].encode('ascii'))
            yield iface

    def lower_iface(iface):
        method_cnt = sum(len(i['methods']) for i in ancestors(iface))
        const_cnt = sum(len(i['consts']) for i in ancestors(iface))

        # The number of maximum methods is not arbitrary. It is the same value
        # as in xpcom/reflect/xptcall/genstubs.pl; do not change this value
        # without changing that one or you WILL see problems.
        #
        # In addition, mNumMethods and mNumConsts are stored as a 8-bit ints,
        # meaning we cannot exceed 255 methods/consts on any interface.
        assert method_cnt < 250, "%s has too many methods" % iface['name']
        assert const_cnt < 256, "%s has too many constants" % iface['name']

        # Store the lowered interface as 'cxx' on the iface object.
        iface['cxx'] = nsXPTInterfaceInfo(
            "%d = %s" % (iface['idx'], iface['name']),

            mIID=lower_uuid(iface['uuid']),
            mName=lower_string(iface['name']),
            mParent=interface_idx(iface['parent']),

            mMethods=len(methods),
            mNumMethods=method_cnt,
            mConsts=len(consts),
            mNumConsts=const_cnt,

            # Flags
            mBuiltinClass='builtinclass' in iface['flags'],
            mMainProcessScriptableOnly='main_process_only' in iface['flags'],
            mFunction='function' in iface['flags'],
        )

        # Lower methods and constants used by this interface
        for method in iface['methods']:
            lower_method(method, iface['name'])
        for const in iface['consts']:
            lower_const(const, iface['name'])

    # Lower the types which have fixed indexes first, and check that the indexes
    # seem correct.
    for expected, ty in enumerate(utility_types):
        got = lower_extra_type(ty)
        assert got == expected, "Wrong index when lowering"

    # Lower interfaces in the order of the IID phf's entries lookup.
    for iface in iid_phf.entries:
        lower_iface(iface)

    # Write out the final output file
    fd.write("/* THIS FILE WAS GENERATED BY xptcodegen.py - DO NOT EDIT */\n\n")

    # Include any bindings files which we need to include for webidl types
    for include in sorted(includes):
        fd.write('#include "%s"\n' % include)

    # Write out our header
    fd.write("""
#include "xptinfo.h"
#include "mozilla/PerfectHash.h"
#include "mozilla/TypeTraits.h"
#include "mozilla/dom/BindingUtils.h"

// These template methods are specialized to be used in the sDOMObjects table.
template<mozilla::dom::prototypes::ID PrototypeID, typename T>
static nsresult UnwrapDOMObject(JS::HandleValue aHandle, void** aObj)
{
  RefPtr<T> p;
  nsresult rv = mozilla::dom::UnwrapObject<PrototypeID, T>(aHandle, p);
  p.forget(aObj);
  return rv;
}

template<typename T>
static bool WrapDOMObject(JSContext* aCx, void* aObj, JS::MutableHandleValue aHandle)
{
  return mozilla::dom::GetOrCreateDOMReflector(aCx, reinterpret_cast<T*>(aObj), aHandle);
}

template<typename T>
static void CleanupDOMObject(void* aObj)
{
  RefPtr<T> p = already_AddRefed<T>(reinterpret_cast<T*>(aObj));
}

namespace xpt {
namespace detail {

""")

    # Static data arrays
    def array(ty, name, els):
        fd.write("const %s %s[] = {%s\n};\n\n" %
                 (ty, name, ','.join(indented('\n' + str(e)) for e in els)))
    array("nsXPTType", "sTypes", types)
    array("nsXPTParamInfo", "sParams", params)
    array("nsXPTMethodInfo", "sMethods", methods)
    array("nsXPTDOMObjectInfo", "sDOMObjects", domobjects)
    array("nsXPTConstantInfo", "sConsts", consts)

    # The strings array. We write out individual characters to avoid MSVC restrictions.
    fd.write("const char sStrings[] = {\n")
    for s, off in strings.iteritems():
        fd.write("  // %d = %s\n  '%s','\\0',\n" % (off, s, "','".join(s)))
    fd.write("};\n\n")

    # Build the perfect hash table for InterfaceByIID
    fd.write(iid_phf.cxx_codegen(
        name='InterfaceByIID',
        entry_type='nsXPTInterfaceInfo',
        entries_name='sInterfaces',
        lower_entry=lambda iface: iface['cxx'],

        # Check that the IIDs match to support IID keys not in the map.
        return_type='const nsXPTInterfaceInfo*',
        return_entry='return entry.IID().Equals(aKey) ? &entry : nullptr;',

        key_type='const nsIID&',
        key_bytes='reinterpret_cast<const char*>(&aKey)',
        key_length='sizeof(nsIID)'))
    fd.write('\n')

    # Build the perfect hash table for InterfaceByName
    fd.write(name_phf.cxx_codegen(
        name='InterfaceByName',
        entry_type='uint16_t',
        lower_entry=lambda iface: '%-4d /* %s */' % (iface['idx'], iface['name']),

        # Get the actual nsXPTInterfaceInfo from sInterfaces, and
        # double-check that names match.
        return_type='const nsXPTInterfaceInfo*',
        return_entry='return strcmp(sInterfaces[entry].Name(), aKey) == 0'
                     ' ? &sInterfaces[entry] : nullptr;'))
    fd.write('\n')

    # Generate some checks that the indexes for the utility types match the
    # declared ones in xptinfo.h
    for idx, ty in enumerate(utility_types):
        fd.write("static_assert(%d == (uint8_t)nsXPTType::Idx::%s, \"Bad idx\");\n" %
                 (idx, ty['tag'][3:]))

    fd.write("""
const uint16_t sInterfacesSize = mozilla::ArrayLength(sInterfaces);

} // namespace detail
} // namespace xpt
""")


def link_and_write(files, outfile):
    interfaces = []
    for file in files:
        with open(file, 'r') as fd:
            interfaces += json.load(fd)

    iids = set()
    names = set()
    for interface in interfaces:
        assert interface['uuid'] not in iids, "duplicated UUID %s" % interface['uuid']
        assert interface['name'] not in names, "duplicated name %s" % interface['name']
        iids.add(interface['uuid'])
        names.add(interface['name'])

    link_to_cpp(interfaces, outfile)


def main():
    from argparse import ArgumentParser
    import sys

    parser = ArgumentParser()
    parser.add_argument('outfile', help='Output C++ file to generate')
    parser.add_argument('xpts', nargs='*', help='source xpt files')

    args = parser.parse_args(sys.argv[1:])
    with open(args.outfile, 'w') as fd:
        link_and_write(args.xpts, fd)


if __name__ == '__main__':
    main()