blob: 11b4d9884e3473eabd816d5e776c3edf85c8f7bd [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This script should be run manually on occasion to make sure all PPAPI types
have appropriate size checking.
"""
import optparse
import os
import subprocess
import sys
# The string that the PrintNamesAndSizes plugin uses to indicate a type is
# expected to have architecture-dependent size.
ARCH_DEPENDENT_STRING = "ArchDependentSize"
COPYRIGHT_STRING_C = (
"""/* Copyright (c) %s The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* This file has compile assertions for the sizes of types that are dependent
* on the architecture for which they are compiled (i.e., 32-bit vs 64-bit).
*/
""") % datetime.date.today().year
class SourceLocation(object):
"""A class representing the source location of a definiton."""
def __init__(self, filename="", start_line=-1, end_line=-1):
self.filename = os.path.normpath(filename)
self.start_line = start_line
self.end_line = end_line
class TypeInfo(object):
"""A class representing information about a C++ type. It contains the
following fields:
- kind: The Clang TypeClassName (Record, Enum, Typedef, Union, etc)
- name: The unmangled string name of the type.
- size: The size in bytes of the type.
- arch_dependent: True if the type may have architecture dependent size
according to PrintNamesAndSizes. False otherwise. Types
which are considered architecture-dependent from 32-bit
to 64-bit are pointers, longs, unsigned longs, and any
type that contains an architecture-dependent type.
- source_location: A SourceLocation describing where the type is defined.
- target: The target Clang was compiling when it found the type definition.
This is used only for diagnostic output.
- parsed_line: The line which Clang output which was used to create this
TypeInfo (as the info_string parameter to __init__). This is
used only for diagnostic output.
"""
def __init__(self, info_string, target):
"""Create a TypeInfo from a given info_string. Also store the name of the
target for which the TypeInfo was first created just so we can print useful
error information.
info_string is a comma-delimited string of the following form:
kind,name,size,arch_dependent,source_file,start_line,end_line
Where:
- kind: The Clang TypeClassName (Record, Enum, Typedef, Union, etc)
- name: The unmangled string name of the type.
- size: The size in bytes of the type.
- arch_dependent: 'ArchDependentSize' if the type has architecture-dependent
size, NotArchDependentSize otherwise.
- source_file: The source file in which the type is defined.
- first_line: The first line of the definition (counting from 0).
- last_line: The last line of the definition (counting from 0).
This should match the output of the PrintNamesAndSizes plugin.
"""
[self.kind, self.name, self.size, arch_dependent_string, source_file,
start_line, end_line] = info_string.split(',')
self.target = target
self.parsed_line = info_string
# Note that Clang counts line numbers from 1, but we want to count from 0.
self.source_location = SourceLocation(source_file,
int(start_line)-1,
int(end_line)-1)
self.arch_dependent = (arch_dependent_string == ARCH_DEPENDENT_STRING)
class FilePatch(object):
"""A class representing a set of line-by-line changes to a particular file.
None of the changes are applied until Apply is called. All line numbers are
counted from 0.
"""
def __init__(self, filename):
self.filename = filename
self.linenums_to_delete = set()
# A dictionary from line number to an array of strings to be inserted at
# that line number.
self.lines_to_add = {}
def Delete(self, start_line, end_line):
"""Make the patch delete the lines starting with |start_line| up to but not
including |end_line|.
"""
self.linenums_to_delete |= set(range(start_line, end_line))
def Add(self, text, line_number):
"""Add the given text before the text on the given line number."""
if line_number in self.lines_to_add:
self.lines_to_add[line_number].append(text)
else:
self.lines_to_add[line_number] = [text]
def Apply(self):
"""Apply the patch by writing it to self.filename."""
# Read the lines of the existing file in to a list.
sourcefile = open(self.filename, "r")
file_lines = sourcefile.readlines()
sourcefile.close()
# Now apply the patch. Our strategy is to keep the array at the same size,
# and just edit strings in the file_lines list as necessary. When we delete
# lines, we just blank the line and keep it in the list. When we add lines,
# we just prepend the added source code to the start of the existing line at
# that line number. This way, all the line numbers we cached from calls to
# Add and Delete remain valid list indices, and we don't have to worry about
# maintaining any offsets. Each element of file_lines at the end may
# contain any number of lines (0 or more) delimited by carriage returns.
for linenum_to_delete in self.linenums_to_delete:
file_lines[linenum_to_delete] = "";
for linenum, sourcelines in self.lines_to_add.items():
# Sort the lines we're adding so we get relatively consistent results.
sourcelines.sort()
# Prepend the new lines. When we output
file_lines[linenum] = "".join(sourcelines) + file_lines[linenum]
newsource = open(self.filename, "w")
for line in file_lines:
newsource.write(line)
newsource.close()
def CheckAndInsert(typeinfo, typeinfo_map):
"""Check if a TypeInfo exists already in the given map with the same name. If
so, make sure the size is consistent.
- If the name exists but the sizes do not match, print a message and
exit with non-zero exit code.
- If the name exists and the sizes match, do nothing.
- If the name does not exist, insert the typeinfo in to the map.
"""
# If the type is unnamed, ignore it.
if typeinfo.name == "":
return
# If the size is 0, ignore it.
elif int(typeinfo.size) == 0:
return
# If the type is not defined under ppapi, ignore it.
elif typeinfo.source_location.filename.find("ppapi") == -1:
return
# If the type is defined under GLES2, ignore it.
elif typeinfo.source_location.filename.find("GLES2") > -1:
return
# If the type is an interface (by convention, starts with PPP_ or PPB_),
# ignore it.
elif (typeinfo.name[:4] == "PPP_") or (typeinfo.name[:4] == "PPB_"):
return
elif typeinfo.name in typeinfo_map:
if typeinfo.size != typeinfo_map[typeinfo.name].size:
print "Error: '" + typeinfo.name + "' is", \
typeinfo_map[typeinfo.name].size, \
"bytes on target '" + typeinfo_map[typeinfo.name].target + \
"', but", typeinfo.size, "on target '" + typeinfo.target + "'"
print typeinfo_map[typeinfo.name].parsed_line
print typeinfo.parsed_line
sys.exit(1)
else:
# It's already in the map and the sizes match.
pass
else:
typeinfo_map[typeinfo.name] = typeinfo
def ProcessTarget(clang_command, target, types):
"""Run clang using the given clang_command for the given target string. Parse
the output to create TypeInfos for each discovered type. Insert each type in
to the 'types' dictionary. If the type already exists in the types
dictionary, make sure that the size matches what's already in the map. If
not, exit with an error message.
"""
p = subprocess.Popen(clang_command + " -triple " + target,
shell=True,
stdout=subprocess.PIPE)
lines = p.communicate()[0].split()
for line in lines:
typeinfo = TypeInfo(line, target)
CheckAndInsert(typeinfo, types)
def ToAssertionCode(typeinfo):
"""Convert the TypeInfo to an appropriate C compile assertion.
If it's a struct (Record in Clang terminology), we want a line like this:
PP_COMPILE_ASSERT_STRUCT_SIZE_IN_BYTES(<name>, <size>);\n
Enums:
PP_COMPILE_ASSERT_ENUM_SIZE_IN_BYTES(<name>, <size>);\n
Typedefs:
PP_COMPILE_ASSERT_SIZE_IN_BYTES(<name>, <size>);\n
"""
line = "PP_COMPILE_ASSERT_"
if typeinfo.kind == "Enum":
line += "ENUM_"
elif typeinfo.kind == "Record":
line += "STRUCT_"
line += "SIZE_IN_BYTES("
line += typeinfo.name
line += ", "
line += typeinfo.size
line += ");\n"
return line
def IsMacroDefinedName(typename):
"""Return true iff the given typename came from a PPAPI compile assertion."""
return typename.find("PP_Dummy_Struct_For_") == 0
def WriteArchSpecificCode(types, root, filename):
"""Write a header file that contains a compile-time assertion for the size of
each of the given typeinfos, in to a file named filename rooted at root.
"""
assertion_lines = [ToAssertionCode(typeinfo) for typeinfo in types]
assertion_lines.sort()
outfile = open(os.path.join(root, filename), "w")
header_guard = "PPAPI_TESTS_" + filename.upper().replace(".", "_") + "_"
outfile.write(COPYRIGHT_STRING_C)
outfile.write('#ifndef ' + header_guard + '\n')
outfile.write('#define ' + header_guard + '\n\n')
outfile.write('#include "ppapi/tests/test_struct_sizes.c"\n\n')
for line in assertion_lines:
outfile.write(line)
outfile.write('\n#endif /* ' + header_guard + ' */\n')
def main(argv):
# See README file for example command-line invocation. This script runs the
# PrintNamesAndSizes Clang plugin with 'test_struct_sizes.c' as input, which
# should include all C headers and all existing size checks. It runs the
# plugin multiple times; once for each of a set of targets, some 32-bit and
# some 64-bit. It verifies that wherever possible, types have a consistent
# size on both platforms. Types that can't easily have consistent size (e.g.
# ones that contain a pointer) are checked to make sure they are consistent
# for all 32-bit platforms and consistent on all 64-bit platforms, but the
# sizes on 32 vs 64 are allowed to differ.
#
# Then, if all the types have consistent size as expected, compile assertions
# are added to the source code. Types whose size is independent of
# architectureacross have their compile assertions placed immediately after
# their definition in the C API header. Types whose size differs on 32-bit
# vs 64-bit have a compile assertion placed in each of:
# ppapi/tests/arch_dependent_sizes_32.h and
# ppapi/tests/arch_dependent_sizes_64.h.
#
# Note that you should always check the results of the tool to make sure
# they are sane.
parser = optparse.OptionParser()
parser.add_option(
'-c', '--clang-path', dest='clang_path',
default=(''),
help='the path to the clang binary (default is to get it from your path)')
parser.add_option(
'-p', '--plugin', dest='plugin',
default='tests/clang/libPrintNamesAndSizes.so',
help='The path to the PrintNamesAndSizes plugin library.')
parser.add_option(
'--targets32', dest='targets32',
default='i386-pc-linux,arm-pc-linux,i386-pc-win32',
help='Which 32-bit target triples to provide to clang.')
parser.add_option(
'--targets64', dest='targets64',
default='x86_64-pc-linux,x86_64-pc-win',
help='Which 32-bit target triples to provide to clang.')
parser.add_option(
'-r', '--ppapi-root', dest='ppapi_root',
default='.',
help='The root directory of ppapi.')
options, args = parser.parse_args(argv)
if args:
parser.print_help()
print 'ERROR: invalid argument'
sys.exit(1)
clang_executable = os.path.join(options.clang_path, 'clang')
clang_command = clang_executable + " -cc1" \
+ " -load " + options.plugin \
+ " -plugin PrintNamesAndSizes" \
+ " -I" + os.path.join(options.ppapi_root, "..") \
+ " " \
+ os.path.join(options.ppapi_root, "tests", "test_struct_sizes.c")
# Dictionaries mapping type names to TypeInfo objects.
# Types that have size dependent on architecture, for 32-bit
types32 = {}
# Types that have size dependent on architecture, for 64-bit
types64 = {}
# Note that types32 and types64 should contain the same types, but with
# different sizes.
# Types whose size should be consistent regardless of architecture.
types_independent = {}
# Now run clang for each target. Along the way, make sure architecture-
# dependent types are consistent sizes on all 32-bit platforms and consistent
# on all 64-bit platforms.
targets32 = options.targets32.split(',');
for target in targets32:
# For each 32-bit target, run the PrintNamesAndSizes Clang plugin to get
# information about all types in the translation unit, and add a TypeInfo
# for each of them to types32. If any size mismatches are found,
# ProcessTarget will spit out an error and exit.
ProcessTarget(clang_command, target, types32)
targets64 = options.targets64.split(',');
for target in targets64:
# Do the same as above for each 64-bit target; put all types in types64.
ProcessTarget(clang_command, target, types64)
# Now for each dictionary, find types whose size are consistent regardless of
# architecture, and move those in to types_independent. Anywhere sizes
# differ, make sure they are expected to be architecture-dependent based on
# their structure. If we find types which could easily be consistent but
# aren't, spit out an error and exit.
types_independent = {}
for typename, typeinfo32 in types32.items():
if (typename in types64):
typeinfo64 = types64[typename]
if (typeinfo64.size == typeinfo32.size):
# The types are the same size, so we can treat it as arch-independent.
types_independent[typename] = typeinfo32
del types32[typename]
del types64[typename]
elif (typeinfo32.arch_dependent or typeinfo64.arch_dependent):
# The type is defined in such a way that it would be difficult to make
# its size consistent. E.g., it has pointers. We'll leave it in the
# arch-dependent maps so that we can put arch-dependent size checks in
# test code.
pass
else:
# The sizes don't match, but there's no reason they couldn't. It's
# probably due to an alignment mismatch between Win32/NaCl vs Linux32/
# Mac32.
print "Error: '" + typename + "' is", typeinfo32.size, \
"bytes on target '" + typeinfo32.target + \
"', but", typeinfo64.size, "on target '" + typeinfo64.target + "'"
print typeinfo32.parsed_line
print typeinfo64.parsed_line
sys.exit(1)
else:
print "WARNING: Type '", typename, "' was defined for target '",
print typeinfo32.target, ", but not for any 64-bit targets."
# Now we have all the information we need to generate our static assertions.
# Types that have consistent size across architectures will have the static
# assertion placed immediately after their definition. Types whose size
# depends on 32-bit vs 64-bit architecture will have checks placed in
# tests/arch_dependent_sizes_32/64.h.
# This dictionary maps file names to FilePatch objects. We will add items
# to it as needed. Each FilePatch represents a set of changes to make to the
# associated file (additions and deletions).
file_patches = {}
# Find locations of existing macros, and just delete them all. Note that
# normally, only things in 'types_independent' need to be deleted, as arch-
# dependent checks exist in tests/arch_dependent_sizes_32/64.h, which are
# always completely over-ridden. However, it's possible that a type that used
# to be arch-independent has changed to now be arch-dependent (e.g., because
# a pointer was added), and we want to delete the old check in that case.
for name, typeinfo in \
types_independent.items() + types32.items() + types64.items():
if IsMacroDefinedName(name):
sourcefile = typeinfo.source_location.filename
if sourcefile not in file_patches:
file_patches[sourcefile] = FilePatch(sourcefile)
file_patches[sourcefile].Delete(typeinfo.source_location.start_line,
typeinfo.source_location.end_line+1)
# Add a compile-time assertion for each type whose size is independent of
# architecture. These assertions go immediately after the class definition.
for name, typeinfo in types_independent.items():
# Ignore dummy types that were defined by macros and also ignore types that
# are 0 bytes (i.e., typedefs to void).
if not IsMacroDefinedName(name) and typeinfo.size > 0:
sourcefile = typeinfo.source_location.filename
if sourcefile not in file_patches:
file_patches[sourcefile] = FilePatch(sourcefile)
# Add the assertion code just after the definition of the type.
# E.g.:
# struct Foo {
# int32_t x;
# };
# PP_COMPILE_ASSERT_STRUCT_SIZE_IN_BYTES(Foo, 4); <---Add this line
file_patches[sourcefile].Add(ToAssertionCode(typeinfo),
typeinfo.source_location.end_line+1)
# Apply our patches. This actually edits the files containing the definitions
# for the types in types_independent.
for filename, patch in file_patches.items():
patch.Apply()
# Write out a file of checks for 32-bit architectures and a separate file for
# 64-bit architectures. These only have checks for types that are
# architecture-dependent.
c_source_root = os.path.join(options.ppapi_root, "tests")
WriteArchSpecificCode(types32.values(),
c_source_root,
"arch_dependent_sizes_32.h")
WriteArchSpecificCode(types64.values(),
c_source_root,
"arch_dependent_sizes_64.h")
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))