summaryrefslogtreecommitdiffstats
path: root/ppapi/generate_ppapi_size_checks.py
blob: 11b4d9884e3473eabd816d5e776c3edf85c8f7bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
#!/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:]))