idl_thunk.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6""" Generator for C++ style thunks """
7
8import glob
9import os
10import re
11import sys
12
13from idl_log import ErrOut, InfoOut, WarnOut
14from idl_node import IDLAttribute, IDLNode
15from idl_ast import IDLAst
16from idl_option import GetOption, Option, ParseOptions
17from idl_outfile import IDLOutFile
18from idl_parser import ParseFiles
19from idl_c_proto import CGen, GetNodeComments, CommentLines, Comment
20from idl_generator import Generator, GeneratorByFile
21
22Option('thunkroot', 'Base directory of output',
23       default=os.path.join('..', 'thunk'))
24
25
26class TGenError(Exception):
27  def __init__(self, msg):
28    self.value = msg
29
30  def __str__(self):
31    return repr(self.value)
32
33
34class ThunkBodyMetadata(object):
35  """Metadata about thunk body. Used for selecting which headers to emit."""
36  def __init__(self):
37    self._apis = set()
38    self._includes = set()
39
40  def AddApi(self, api):
41    self._apis.add(api)
42
43  def Apis(self):
44    return self._apis
45
46  def AddInclude(self, include):
47    self._includes.add(include)
48
49  def Includes(self):
50    return self._includes
51
52
53def _GetBaseFileName(filenode):
54  """Returns the base name for output files, given the filenode.
55
56  Examples:
57    'dev/ppb_find_dev.h' -> 'ppb_find_dev'
58    'trusted/ppb_buffer_trusted.h' -> 'ppb_buffer_trusted'
59  """
60  path, name = os.path.split(filenode.GetProperty('NAME'))
61  name = os.path.splitext(name)[0]
62  return name
63
64
65def _GetHeaderFileName(filenode):
66  """Returns the name for the header for this file."""
67  path, name = os.path.split(filenode.GetProperty('NAME'))
68  name = os.path.splitext(name)[0]
69  if path:
70    header = "ppapi/c/%s/%s.h" % (path, name)
71  else:
72    header = "ppapi/c/%s.h" % name
73  return header
74
75
76def _GetThunkFileName(filenode, relpath):
77  """Returns the thunk file name."""
78  path = os.path.split(filenode.GetProperty('NAME'))[0]
79  name = _GetBaseFileName(filenode)
80  # We don't reattach the path for thunk.
81  if relpath: name = os.path.join(relpath, name)
82  name = '%s%s' % (name, '_thunk.cc')
83  return name
84
85
86def _MakeEnterLine(filenode, interface, arg, handle_errors, callback, meta):
87  """Returns an EnterInstance/EnterResource string for a function."""
88  if arg[0] == 'PP_Instance':
89    if callback is None:
90      return 'EnterInstance enter(%s);' % arg[1]
91    else:
92      return 'EnterInstance enter(%s, %s);' % (arg[1], callback)
93  elif arg[0] == 'PP_Resource':
94    api_name = interface.GetName()
95    if api_name.endswith('Trusted'):
96      api_name = api_name[:-len('Trusted')]
97    if api_name.endswith('_Dev'):
98      api_name = api_name[:-len('_Dev')]
99    api_name += '_API'
100
101    enter_type = 'EnterResource<%s>' % api_name
102    # The API header matches the file name, not the interface name.
103    api_basename = _GetBaseFileName(filenode)
104    if api_basename.endswith('_dev'):
105      # Clip off _dev suffix.
106      api_basename = api_basename[:-len('_dev')]
107    if api_basename.endswith('_trusted'):
108      # Clip off _trusted suffix.
109      api_basename = api_basename[:-len('_trusted')]
110    meta.AddApi(api_basename + '_api')
111
112    if callback is None:
113      return '%s enter(%s, %s);' % (enter_type, arg[1],
114                                    str(handle_errors).lower())
115    else:
116      return '%s enter(%s, %s, %s);' % (enter_type, arg[1],
117                                        callback,
118                                        str(handle_errors).lower())
119  else:
120    raise TGenError("Unknown type for _MakeEnterLine: %s" % arg[0])
121
122
123def _GetShortName(interface, filter_suffixes):
124  """Return a shorter interface name that matches Is* and Create* functions."""
125  parts = interface.GetName().split('_')[1:]
126  tail = parts[len(parts) - 1]
127  if tail in filter_suffixes:
128    parts = parts[:-1]
129  return ''.join(parts)
130
131
132def _IsTypeCheck(interface, node):
133  """Returns true if node represents a type-checking function."""
134  return node.GetName() == 'Is%s' % _GetShortName(interface, ['Dev', 'Private'])
135
136
137def _GetCreateFuncName(interface):
138  """Returns the creation function name for an interface."""
139  return 'Create%s' % _GetShortName(interface, ['Dev'])
140
141
142def _GetDefaultFailureValue(t):
143  """Returns the default failure value for a given type.
144
145  Returns None if no default failure value exists for the type.
146  """
147  values = {
148      'PP_Bool': 'PP_FALSE',
149      'PP_Resource': '0',
150      'struct PP_Var': 'PP_MakeUndefined()',
151      'float': '0.0f',
152      'int32_t': 'enter.retval()',
153      'uint16_t': '0',
154      'uint32_t': '0',
155      'uint64_t': '0',
156  }
157  if t in values:
158    return values[t]
159  return None
160
161
162def _MakeCreateMemberBody(interface, member, args):
163  """Returns the body of a Create() function.
164
165  Args:
166    interface - IDLNode for the interface
167    member - IDLNode for member function
168    args - List of arguments for the Create() function
169  """
170  if args[0][0] == 'PP_Resource':
171    body = 'Resource* object =\n'
172    body += '    PpapiGlobals::Get()->GetResourceTracker()->'
173    body += 'GetResource(%s);\n' % args[0][1]
174    body += 'if (!object)\n'
175    body += '  return 0;\n'
176    body += 'EnterResourceCreation enter(object->pp_instance());\n'
177  elif args[0][0] == 'PP_Instance':
178    body = 'EnterResourceCreation enter(%s);\n' % args[0][1]
179  else:
180    raise TGenError('Unknown arg type for Create(): %s' % args[0][0])
181
182  body += 'if (enter.failed())\n'
183  body += '  return 0;\n'
184  arg_list = ', '.join([a[1] for a in args])
185  if member.GetProperty('create_func'):
186    create_func = member.GetProperty('create_func')
187  else:
188    create_func = _GetCreateFuncName(interface)
189  body += 'return enter.functions()->%s(%s);' % (create_func,
190                                                 arg_list)
191  return body
192
193
194def _MakeNormalMemberBody(filenode, release, node, member, rtype, args,
195                          include_version, meta):
196  """Returns the body of a typical function.
197
198  Args:
199    filenode - IDLNode for the file
200    release - release to generate body for
201    node - IDLNode for the interface
202    member - IDLNode for the member function
203    rtype - Return type for the member function
204    args - List of 4-tuple arguments for the member function
205    include_version - whether to include the version in the invocation
206    meta - ThunkBodyMetadata for header hints
207  """
208  is_callback_func = args[len(args) - 1][0] == 'struct PP_CompletionCallback'
209
210  if is_callback_func:
211    call_args = args[:-1] + [('', 'enter.callback()', '', '')]
212    meta.AddInclude('ppapi/c/pp_completion_callback.h')
213  else:
214    call_args = args
215
216  if args[0][0] == 'PP_Instance':
217    call_arglist = ', '.join(a[1] for a in call_args)
218    function_container = 'functions'
219  else:
220    call_arglist = ', '.join(a[1] for a in call_args[1:])
221    function_container = 'object'
222
223  function_name = member.GetName()
224  if include_version:
225    version = node.GetVersion(release).replace('.', '_')
226    function_name += version
227
228  invocation = 'enter.%s()->%s(%s)' % (function_container,
229                                       function_name,
230                                       call_arglist)
231
232  handle_errors = not (member.GetProperty('report_errors') == 'False')
233  if is_callback_func:
234    body = '%s\n' % _MakeEnterLine(filenode, node, args[0], handle_errors,
235                                   args[len(args) - 1][1], meta)
236    body += 'if (enter.failed())\n'
237    value = member.GetProperty('on_failure')
238    if value is None:
239      value = 'enter.retval()'
240    body += '  return %s;\n' % value
241    body += 'return enter.SetResult(%s);' % invocation
242  elif rtype == 'void':
243    body = '%s\n' % _MakeEnterLine(filenode, node, args[0], handle_errors,
244                                   None, meta)
245    body += 'if (enter.succeeded())\n'
246    body += '  %s;' % invocation
247  else:
248    value = member.GetProperty('on_failure')
249    if value is None:
250      value = _GetDefaultFailureValue(rtype)
251    if value is None:
252      raise TGenError('No default value for rtype %s' % rtype)
253
254    body = '%s\n' % _MakeEnterLine(filenode, node, args[0], handle_errors,
255                                   None, meta)
256    body += 'if (enter.failed())\n'
257    body += '  return %s;\n' % value
258    body += 'return %s;' % invocation
259  return body
260
261
262def DefineMember(filenode, node, member, release, include_version, meta):
263  """Returns a definition for a member function of an interface.
264
265  Args:
266    filenode - IDLNode for the file
267    node - IDLNode for the interface
268    member - IDLNode for the member function
269    release - release to generate
270    include_version - include the version in emitted function name.
271    meta - ThunkMetadata for header hints
272  Returns:
273    A string with the member definition.
274  """
275  cgen = CGen()
276  rtype, name, arrays, args = cgen.GetComponents(member, release, 'return')
277
278  if _IsTypeCheck(node, member):
279    body = '%s\n' % _MakeEnterLine(filenode, node, args[0], False, None, meta)
280    body += 'return PP_FromBool(enter.succeeded());'
281  elif member.GetName() == 'Create':
282    body = _MakeCreateMemberBody(node, member, args)
283  else:
284    body = _MakeNormalMemberBody(filenode, release, node, member, rtype, args,
285                                 include_version, meta)
286
287  signature = cgen.GetSignature(member, release, 'return', func_as_ptr=False,
288                                include_version=include_version)
289  return '%s\n%s\n}' % (cgen.Indent('%s {' % signature, tabs=0),
290                        cgen.Indent(body, tabs=1))
291
292
293def _IsNewestMember(member, members, releases):
294  """Returns true if member is the newest node with its name in members.
295
296  Currently, every node in the AST only has one version. This means that we
297  will have two sibling nodes with the same name to represent different
298  versions.
299  See http://crbug.com/157017 .
300
301  Special handling is required for nodes which share their name with others,
302  but aren't the newest version in the IDL.
303
304  Args:
305    member - The member which is checked if it's newest
306    members - The list of members to inspect
307    releases - The set of releases to check for versions in.
308  """
309  build_list = member.GetUniqueReleases(releases)
310  release = build_list[0]  # Pick the oldest release.
311  same_name_siblings = filter(
312      lambda n: str(n) == str(member) and n != member, members)
313
314  for s in same_name_siblings:
315    sibling_build_list = s.GetUniqueReleases(releases)
316    sibling_release = sibling_build_list[0]
317    if sibling_release > release:
318      return False
319  return True
320
321
322class TGen(GeneratorByFile):
323  def __init__(self):
324    Generator.__init__(self, 'Thunk', 'tgen', 'Generate the C++ thunk.')
325
326  def GenerateFile(self, filenode, releases, options):
327    savename = _GetThunkFileName(filenode, GetOption('thunkroot'))
328    my_min, my_max = filenode.GetMinMax(releases)
329    if my_min > releases[-1] or my_max < releases[0]:
330      if os.path.isfile(savename):
331        print "Removing stale %s for this range." % filenode.GetName()
332        os.remove(os.path.realpath(savename))
333      return False
334    do_generate = filenode.GetProperty('generate_thunk')
335    if not do_generate:
336      return False
337
338    thunk_out = IDLOutFile(savename)
339    body, meta = self.GenerateBody(thunk_out, filenode, releases, options)
340    self.WriteHead(thunk_out, filenode, releases, options, meta)
341    thunk_out.Write('\n\n'.join(body))
342    self.WriteTail(thunk_out, filenode, releases, options)
343    return thunk_out.Close()
344
345  def WriteHead(self, out, filenode, releases, options, meta):
346    __pychecker__ = 'unusednames=options'
347    cgen = CGen()
348
349    cright_node = filenode.GetChildren()[0]
350    assert(cright_node.IsA('Copyright'))
351    out.Write('%s\n' % cgen.Copyright(cright_node, cpp_style=True))
352
353    # Wrap the From ... modified ... comment if it would be >80 characters.
354    from_text = 'From %s' % (
355        filenode.GetProperty('NAME').replace(os.sep,'/'))
356    modified_text = 'modified %s.' % (
357        filenode.GetProperty('DATETIME'))
358    if len(from_text) + len(modified_text) < 74:
359      out.Write('// %s %s\n\n' % (from_text, modified_text))
360    else:
361      out.Write('// %s,\n//   %s\n\n' % (from_text, modified_text))
362
363
364    # TODO(teravest): Don't emit includes we don't need.
365    includes = ['ppapi/c/pp_errors.h',
366                'ppapi/shared_impl/tracked_callback.h',
367                'ppapi/thunk/enter.h',
368                'ppapi/thunk/ppb_instance_api.h',
369                'ppapi/thunk/resource_creation_api.h',
370                'ppapi/thunk/thunk.h']
371    includes.append(_GetHeaderFileName(filenode))
372    for api in meta.Apis():
373      includes.append('ppapi/thunk/%s.h' % api.lower())
374    for i in meta.Includes():
375      includes.append(i)
376    for include in sorted(includes):
377      out.Write('#include "%s"\n' % include)
378    out.Write('\n')
379    out.Write('namespace ppapi {\n')
380    out.Write('namespace thunk {\n')
381    out.Write('\n')
382    out.Write('namespace {\n')
383    out.Write('\n')
384
385  def GenerateBody(self, out, filenode, releases, options):
386    """Generates a member function lines to be written and metadata.
387
388    Returns a tuple of (body, meta) where:
389      body - a list of lines with member function bodies
390      meta - a ThunkMetadata instance for hinting which headers are needed.
391    """
392    __pychecker__ = 'unusednames=options'
393    out_members = []
394    meta = ThunkBodyMetadata()
395    for node in filenode.GetListOf('Interface'):
396      # Skip if this node is not in this release
397      if not node.InReleases(releases):
398        print "Skipping %s" % node
399        continue
400
401      # Generate Member functions
402      if node.IsA('Interface'):
403        members = node.GetListOf('Member')
404        for child in members:
405          build_list = child.GetUniqueReleases(releases)
406          # We have to filter out releases this node isn't in.
407          build_list = filter(lambda r: child.InReleases([r]), build_list)
408          if len(build_list) == 0:
409            continue
410          assert(len(build_list) == 1)
411          release = build_list[-1]
412          include_version = not _IsNewestMember(child, members, releases)
413          member = DefineMember(filenode, node, child, release, include_version,
414                                meta)
415          if not member:
416            continue
417          out_members.append(member)
418    return (out_members, meta)
419
420  def WriteTail(self, out, filenode, releases, options):
421    __pychecker__ = 'unusednames=options'
422    cgen = CGen()
423
424    version_list = []
425    out.Write('\n\n')
426    for node in filenode.GetListOf('Interface'):
427      build_list = node.GetUniqueReleases(releases)
428      for build in build_list:
429        version = node.GetVersion(build).replace('.', '_')
430        thunk_name = 'g_' + node.GetName().lower() + '_thunk_' + \
431                      version
432        thunk_type = '_'.join((node.GetName(), version))
433        version_list.append((thunk_type, thunk_name))
434
435        declare_line = 'const %s %s = {' % (thunk_type, thunk_name)
436        if len(declare_line) > 80:
437          declare_line = 'const %s\n    %s = {' % (thunk_type, thunk_name)
438        out.Write('%s\n' % declare_line)
439        generated_functions = []
440        members = node.GetListOf('Member')
441        for child in members:
442          rtype, name, arrays, args = cgen.GetComponents(
443              child, build, 'return')
444          if not _IsNewestMember(child, members, releases):
445            version = node.GetVersion(build).replace('.', '_')
446            name += '_' + version
447          if child.InReleases([build]):
448            generated_functions.append(name)
449        out.Write(',\n'.join(['  &%s' % f for f in generated_functions]))
450        out.Write('\n};\n\n')
451
452    out.Write('}  // namespace\n')
453    out.Write('\n')
454    for thunk_type, thunk_name in version_list:
455      thunk_decl = 'const %s* Get%s_Thunk() {\n' % (thunk_type, thunk_type)
456      if len(thunk_decl) > 80:
457        thunk_decl = 'const %s*\n    Get%s_Thunk() {\n' % (thunk_type,
458                                                           thunk_type)
459      out.Write(thunk_decl)
460      out.Write('  return &%s;\n' % thunk_name)
461      out.Write('}\n')
462      out.Write('\n')
463    out.Write('}  // namespace thunk\n')
464    out.Write('}  // namespace ppapi\n')
465
466
467tgen = TGen()
468
469
470def Main(args):
471  # Default invocation will verify the golden files are unchanged.
472  failed = 0
473  if not args:
474    args = ['--wnone', '--diff', '--test', '--thunkroot=.']
475
476  ParseOptions(args)
477
478  idldir = os.path.split(sys.argv[0])[0]
479  idldir = os.path.join(idldir, 'test_thunk', '*.idl')
480  filenames = glob.glob(idldir)
481  ast = ParseFiles(filenames)
482  if tgen.GenerateRange(ast, ['M13', 'M14'], {}):
483    print "Golden file for M13-M14 failed."
484    failed = 1
485  else:
486    print "Golden file for M13-M14 passed."
487
488  return failed
489
490
491if __name__ == '__main__':
492  sys.exit(Main(sys.argv[1:]))
493