idl_thunk.py revision eb525c5499e34cc9c4b825d6d9e75bb07cc06ace
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._builtin_includes = set()
39    self._includes = set()
40
41  def AddApi(self, api):
42    self._apis.add(api)
43
44  def Apis(self):
45    return self._apis
46
47  def AddInclude(self, include):
48    self._includes.add(include)
49
50  def Includes(self):
51    return self._includes
52
53  def AddBuiltinInclude(self, include):
54    self._builtin_includes.add(include)
55
56  def BuiltinIncludes(self):
57    return self._builtin_includes
58
59
60def _GetBaseFileName(filenode):
61  """Returns the base name for output files, given the filenode.
62
63  Examples:
64    'dev/ppb_find_dev.h' -> 'ppb_find_dev'
65    'trusted/ppb_buffer_trusted.h' -> 'ppb_buffer_trusted'
66  """
67  path, name = os.path.split(filenode.GetProperty('NAME'))
68  name = os.path.splitext(name)[0]
69  return name
70
71
72def _GetHeaderFileName(filenode):
73  """Returns the name for the header for this file."""
74  path, name = os.path.split(filenode.GetProperty('NAME'))
75  name = os.path.splitext(name)[0]
76  if path:
77    header = "ppapi/c/%s/%s.h" % (path, name)
78  else:
79    header = "ppapi/c/%s.h" % name
80  return header
81
82
83def _GetThunkFileName(filenode, relpath):
84  """Returns the thunk file name."""
85  path = os.path.split(filenode.GetProperty('NAME'))[0]
86  name = _GetBaseFileName(filenode)
87  # We don't reattach the path for thunk.
88  if relpath: name = os.path.join(relpath, name)
89  name = '%s%s' % (name, '_thunk.cc')
90  return name
91
92
93def _StripFileName(filenode):
94  """Strips path  and dev, trusted, and private suffixes from the file name."""
95  api_basename = _GetBaseFileName(filenode)
96  if api_basename.endswith('_dev'):
97    api_basename = api_basename[:-len('_dev')]
98  if api_basename.endswith('_trusted'):
99    api_basename = api_basename[:-len('_trusted')]
100  if api_basename.endswith('_private'):
101    api_basename = api_basename[:-len('_private')]
102  return api_basename
103
104
105def _StripApiName(api_name):
106  """Strips Dev, Private, and Trusted suffixes from the API name."""
107  if api_name.endswith('Trusted'):
108    api_name = api_name[:-len('Trusted')]
109  if api_name.endswith('_Dev'):
110    api_name = api_name[:-len('_Dev')]
111  if api_name.endswith('_Private'):
112    api_name = api_name[:-len('_Private')]
113  return api_name
114
115
116def _MakeEnterLine(filenode, interface, member, arg, handle_errors, callback,
117                   meta):
118  """Returns an EnterInstance/EnterResource string for a function."""
119  api_name = _StripApiName(interface.GetName()) + '_API'
120  if member.GetProperty('api'):  # Override API name.
121    manually_provided_api = True
122    # TODO(teravest): Automatically guess the API header file.
123    api_name = member.GetProperty('api')
124  else:
125    manually_provided_api = False
126
127  if arg[0] == 'PP_Instance':
128    if callback is None:
129      arg_string = arg[1]
130    else:
131      arg_string = '%s, %s' % (arg[1], callback)
132    if interface.GetProperty('singleton') or member.GetProperty('singleton'):
133      if not manually_provided_api:
134        meta.AddApi('ppapi/thunk/%s_api.h' % _StripFileName(filenode))
135      return 'EnterInstanceAPI<%s> enter(%s);' % (api_name, arg_string)
136    else:
137      return 'EnterInstance enter(%s);' % arg_string
138  elif arg[0] == 'PP_Resource':
139    enter_type = 'EnterResource<%s>' % api_name
140    if not manually_provided_api:
141      meta.AddApi('ppapi/thunk/%s_api.h' % _StripFileName(filenode))
142    if callback is None:
143      return '%s enter(%s, %s);' % (enter_type, arg[1],
144                                    str(handle_errors).lower())
145    else:
146      return '%s enter(%s, %s, %s);' % (enter_type, arg[1],
147                                        callback,
148                                        str(handle_errors).lower())
149  else:
150    raise TGenError("Unknown type for _MakeEnterLine: %s" % arg[0])
151
152
153def _GetShortName(interface, filter_suffixes):
154  """Return a shorter interface name that matches Is* and Create* functions."""
155  parts = interface.GetName().split('_')[1:]
156  tail = parts[len(parts) - 1]
157  if tail in filter_suffixes:
158    parts = parts[:-1]
159  return ''.join(parts)
160
161
162def _IsTypeCheck(interface, node, args):
163  """Returns true if node represents a type-checking function."""
164  if len(args) == 0 or args[0][0] != 'PP_Resource':
165    return False
166  return node.GetName() == 'Is%s' % _GetShortName(interface, ['Dev', 'Private'])
167
168
169def _GetCreateFuncName(interface):
170  """Returns the creation function name for an interface."""
171  return 'Create%s' % _GetShortName(interface, ['Dev'])
172
173
174def _GetDefaultFailureValue(t):
175  """Returns the default failure value for a given type.
176
177  Returns None if no default failure value exists for the type.
178  """
179  values = {
180      'PP_Bool': 'PP_FALSE',
181      'PP_Resource': '0',
182      'struct PP_Var': 'PP_MakeUndefined()',
183      'float': '0.0f',
184      'int32_t': 'enter.retval()',
185      'uint16_t': '0',
186      'uint32_t': '0',
187      'uint64_t': '0',
188      'void*': 'NULL'
189  }
190  if t in values:
191    return values[t]
192  return None
193
194
195def _MakeCreateMemberBody(interface, member, args):
196  """Returns the body of a Create() function.
197
198  Args:
199    interface - IDLNode for the interface
200    member - IDLNode for member function
201    args - List of arguments for the Create() function
202  """
203  if args[0][0] == 'PP_Resource':
204    body = 'Resource* object =\n'
205    body += '    PpapiGlobals::Get()->GetResourceTracker()->'
206    body += 'GetResource(%s);\n' % args[0][1]
207    body += 'if (!object)\n'
208    body += '  return 0;\n'
209    body += 'EnterResourceCreation enter(object->pp_instance());\n'
210  elif args[0][0] == 'PP_Instance':
211    body = 'EnterResourceCreation enter(%s);\n' % args[0][1]
212  else:
213    raise TGenError('Unknown arg type for Create(): %s' % args[0][0])
214
215  body += 'if (enter.failed())\n'
216  body += '  return 0;\n'
217  arg_list = ', '.join([a[1] for a in args])
218  if member.GetProperty('create_func'):
219    create_func = member.GetProperty('create_func')
220  else:
221    create_func = _GetCreateFuncName(interface)
222  body += 'return enter.functions()->%s(%s);' % (create_func,
223                                                 arg_list)
224  return body
225
226
227def _GetOutputParams(member, release):
228  """Returns output parameters (and their types) for a member function.
229
230  Args:
231    member - IDLNode for the member function
232    release - Release to get output parameters for
233  Returns:
234    A list of name strings for all output parameters of the member
235    function.
236  """
237  out_params = []
238  callnode = member.GetOneOf('Callspec')
239  if callnode:
240    cgen = CGen()
241    for param in callnode.GetListOf('Param'):
242      mode = cgen.GetParamMode(param)
243      if mode == 'out':
244        # We use the 'store' mode when getting the parameter type, since we
245        # need to call sizeof() for memset().
246        _, pname, _, _ = cgen.GetComponents(param, release, 'store')
247        out_params.append(pname)
248  return out_params
249
250
251def _MakeNormalMemberBody(filenode, release, node, member, rtype, args,
252                          include_version, meta):
253  """Returns the body of a typical function.
254
255  Args:
256    filenode - IDLNode for the file
257    release - release to generate body for
258    node - IDLNode for the interface
259    member - IDLNode for the member function
260    rtype - Return type for the member function
261    args - List of 4-tuple arguments for the member function
262    include_version - whether to include the version in the invocation
263    meta - ThunkBodyMetadata for header hints
264  """
265  if len(args) == 0:
266    # Calling into the "Shared" code for the interface seems like a reasonable
267    # heuristic when we don't have any arguments; some thunk code follows this
268    # convention today.
269    meta.AddApi('ppapi/shared_impl/%s_shared.h' % _StripFileName(filenode))
270    return 'return %s::%s();' % (_StripApiName(node.GetName()) + '_Shared',
271                                 member.GetName())
272
273  is_callback_func = args[len(args) - 1][0] == 'struct PP_CompletionCallback'
274
275  if is_callback_func:
276    call_args = args[:-1] + [('', 'enter.callback()', '', '')]
277    meta.AddInclude('ppapi/c/pp_completion_callback.h')
278  else:
279    call_args = args
280
281  if args[0][0] == 'PP_Instance':
282    call_arglist = ', '.join(a[1] for a in call_args)
283    function_container = 'functions'
284  elif args[0][0] == 'PP_Resource':
285    call_arglist = ', '.join(a[1] for a in call_args[1:])
286    function_container = 'object'
287  else:
288    # Calling into the "Shared" code for the interface seems like a reasonable
289    # heuristic when the first argument isn't a PP_Instance or a PP_Resource;
290    # some thunk code follows this convention today.
291    meta.AddApi('ppapi/shared_impl/%s_shared.h' % _StripFileName(filenode))
292    return 'return %s::%s(%s);' % (_StripApiName(node.GetName()) + '_Shared',
293                                   member.GetName(),
294                                   ', '.join(a[1] for a in args))
295
296  function_name = member.GetName()
297  if include_version:
298    version = node.GetVersion(release).replace('.', '_')
299    function_name += version
300
301  invocation = 'enter.%s()->%s(%s)' % (function_container,
302                                       function_name,
303                                       call_arglist)
304
305  handle_errors = not (member.GetProperty('report_errors') == 'False')
306  out_params = _GetOutputParams(member, release)
307  if is_callback_func:
308    body = '%s\n' % _MakeEnterLine(filenode, node, member, args[0],
309                                   handle_errors, args[len(args) - 1][1], meta)
310    failure_value = member.GetProperty('on_failure')
311    if failure_value is None:
312      failure_value = 'enter.retval()'
313    failure_return = 'return %s;' % failure_value
314    success_return = 'return enter.SetResult(%s);' % invocation
315  elif rtype == 'void':
316    body = '%s\n' % _MakeEnterLine(filenode, node, member, args[0],
317                                   handle_errors, None, meta)
318    failure_return = 'return;'
319    success_return = '%s;' % invocation  # We don't return anything for void.
320  else:
321    body = '%s\n' % _MakeEnterLine(filenode, node, member, args[0],
322                                   handle_errors, None, meta)
323    failure_value = member.GetProperty('on_failure')
324    if failure_value is None:
325      failure_value = _GetDefaultFailureValue(rtype)
326    if failure_value is None:
327      raise TGenError('There is no default value for rtype %s. '
328                      'Maybe you should provide an on_failure attribute '
329                      'in the IDL file.' % rtype)
330    failure_return = 'return %s;' % failure_value
331    success_return = 'return %s;' % invocation
332
333  if member.GetProperty('always_set_output_parameters'):
334    body += 'if (enter.failed()) {\n'
335    for param in out_params:
336      body += '  memset(%s, 0, sizeof(*%s));\n' % (param, param)
337    body += '  %s\n' % failure_return
338    body += '}\n'
339    body += '%s' % success_return
340    meta.AddBuiltinInclude('string.h')
341  else:
342    body += 'if (enter.failed())\n'
343    body += '  %s\n' % failure_return
344    body += '%s' % success_return
345  return body
346
347
348def DefineMember(filenode, node, member, release, include_version, meta):
349  """Returns a definition for a member function of an interface.
350
351  Args:
352    filenode - IDLNode for the file
353    node - IDLNode for the interface
354    member - IDLNode for the member function
355    release - release to generate
356    include_version - include the version in emitted function name.
357    meta - ThunkMetadata for header hints
358  Returns:
359    A string with the member definition.
360  """
361  cgen = CGen()
362  rtype, name, arrays, args = cgen.GetComponents(member, release, 'return')
363  body = 'VLOG(4) << \"%s::%s()\";\n' % (node.GetName(), member.GetName())
364
365  if _IsTypeCheck(node, member, args):
366    body += '%s\n' % _MakeEnterLine(filenode, node, member, args[0], False,
367                                    None, meta)
368    body += 'return PP_FromBool(enter.succeeded());'
369  elif member.GetName() == 'Create' or member.GetName() == 'CreateTrusted':
370    body += _MakeCreateMemberBody(node, member, args)
371  else:
372    body += _MakeNormalMemberBody(filenode, release, node, member, rtype, args,
373                                  include_version, meta)
374
375  signature = cgen.GetSignature(member, release, 'return', func_as_ptr=False,
376                                include_version=include_version)
377  return '%s\n%s\n}' % (cgen.Indent('%s {' % signature, tabs=0),
378                        cgen.Indent(body, tabs=1))
379
380
381def _IsNewestMember(member, members, releases):
382  """Returns true if member is the newest node with its name in members.
383
384  Currently, every node in the AST only has one version. This means that we
385  will have two sibling nodes with the same name to represent different
386  versions.
387  See http://crbug.com/157017 .
388
389  Special handling is required for nodes which share their name with others,
390  but aren't the newest version in the IDL.
391
392  Args:
393    member - The member which is checked if it's newest
394    members - The list of members to inspect
395    releases - The set of releases to check for versions in.
396  """
397  build_list = member.GetUniqueReleases(releases)
398  release = build_list[0]  # Pick the oldest release.
399  same_name_siblings = filter(
400      lambda n: str(n) == str(member) and n != member, members)
401
402  for s in same_name_siblings:
403    sibling_build_list = s.GetUniqueReleases(releases)
404    sibling_release = sibling_build_list[0]
405    if sibling_release > release:
406      return False
407  return True
408
409
410class TGen(GeneratorByFile):
411  def __init__(self):
412    Generator.__init__(self, 'Thunk', 'tgen', 'Generate the C++ thunk.')
413
414  def GenerateFile(self, filenode, releases, options):
415    savename = _GetThunkFileName(filenode, GetOption('thunkroot'))
416    my_min, my_max = filenode.GetMinMax(releases)
417    if my_min > releases[-1] or my_max < releases[0]:
418      if os.path.isfile(savename):
419        print "Removing stale %s for this range." % filenode.GetName()
420        os.remove(os.path.realpath(savename))
421      return False
422    do_generate = filenode.GetProperty('generate_thunk')
423    if not do_generate:
424      return False
425
426    thunk_out = IDLOutFile(savename)
427    body, meta = self.GenerateBody(thunk_out, filenode, releases, options)
428    # TODO(teravest): How do we handle repeated values?
429    if filenode.GetProperty('thunk_include'):
430      meta.AddInclude(filenode.GetProperty('thunk_include'))
431    self.WriteHead(thunk_out, filenode, releases, options, meta)
432    thunk_out.Write('\n\n'.join(body))
433    self.WriteTail(thunk_out, filenode, releases, options)
434    return thunk_out.Close()
435
436  def WriteHead(self, out, filenode, releases, options, meta):
437    __pychecker__ = 'unusednames=options'
438    cgen = CGen()
439
440    cright_node = filenode.GetChildren()[0]
441    assert(cright_node.IsA('Copyright'))
442    out.Write('%s\n' % cgen.Copyright(cright_node, cpp_style=True))
443
444    # Wrap the From ... modified ... comment if it would be >80 characters.
445    from_text = 'From %s' % (
446        filenode.GetProperty('NAME').replace(os.sep,'/'))
447    modified_text = 'modified %s.' % (
448        filenode.GetProperty('DATETIME'))
449    if len(from_text) + len(modified_text) < 74:
450      out.Write('// %s %s\n\n' % (from_text, modified_text))
451    else:
452      out.Write('// %s,\n//   %s\n\n' % (from_text, modified_text))
453
454    if meta.BuiltinIncludes():
455      for include in sorted(meta.BuiltinIncludes()):
456        out.Write('#include <%s>\n' % include)
457      out.Write('\n')
458
459    # TODO(teravest): Don't emit includes we don't need.
460    includes = ['ppapi/c/pp_errors.h',
461                'ppapi/shared_impl/tracked_callback.h',
462                'ppapi/thunk/enter.h',
463                'ppapi/thunk/ppb_instance_api.h',
464                'ppapi/thunk/resource_creation_api.h',
465                'ppapi/thunk/thunk.h']
466    includes.append(_GetHeaderFileName(filenode))
467    for api in meta.Apis():
468      includes.append('%s' % api.lower())
469    for i in meta.Includes():
470      includes.append(i)
471    for include in sorted(includes):
472      out.Write('#include "%s"\n' % include)
473    out.Write('\n')
474    out.Write('namespace ppapi {\n')
475    out.Write('namespace thunk {\n')
476    out.Write('\n')
477    out.Write('namespace {\n')
478    out.Write('\n')
479
480  def GenerateBody(self, out, filenode, releases, options):
481    """Generates a member function lines to be written and metadata.
482
483    Returns a tuple of (body, meta) where:
484      body - a list of lines with member function bodies
485      meta - a ThunkMetadata instance for hinting which headers are needed.
486    """
487    __pychecker__ = 'unusednames=options'
488    out_members = []
489    meta = ThunkBodyMetadata()
490    for node in filenode.GetListOf('Interface'):
491      # Skip if this node is not in this release
492      if not node.InReleases(releases):
493        print "Skipping %s" % node
494        continue
495
496      # Generate Member functions
497      if node.IsA('Interface'):
498        members = node.GetListOf('Member')
499        for child in members:
500          build_list = child.GetUniqueReleases(releases)
501          # We have to filter out releases this node isn't in.
502          build_list = filter(lambda r: child.InReleases([r]), build_list)
503          if len(build_list) == 0:
504            continue
505          release = build_list[-1]
506          include_version = not _IsNewestMember(child, members, releases)
507          member = DefineMember(filenode, node, child, release, include_version,
508                                meta)
509          if not member:
510            continue
511          out_members.append(member)
512    return (out_members, meta)
513
514  def WriteTail(self, out, filenode, releases, options):
515    __pychecker__ = 'unusednames=options'
516    cgen = CGen()
517
518    version_list = []
519    out.Write('\n\n')
520    for node in filenode.GetListOf('Interface'):
521      build_list = node.GetUniqueReleases(releases)
522      for build in build_list:
523        version = node.GetVersion(build).replace('.', '_')
524        thunk_name = 'g_' + node.GetName().lower() + '_thunk_' + \
525                      version
526        thunk_type = '_'.join((node.GetName(), version))
527        version_list.append((thunk_type, thunk_name))
528
529        declare_line = 'const %s %s = {' % (thunk_type, thunk_name)
530        if len(declare_line) > 80:
531          declare_line = 'const %s\n    %s = {' % (thunk_type, thunk_name)
532        out.Write('%s\n' % declare_line)
533        generated_functions = []
534        members = node.GetListOf('Member')
535        for child in members:
536          rtype, name, arrays, args = cgen.GetComponents(
537              child, build, 'return')
538          if child.InReleases([build]):
539            if not _IsNewestMember(child, members, releases):
540              version = child.GetVersion(
541                  child.first_release[build]).replace('.', '_')
542              name += '_' + version
543            generated_functions.append(name)
544        out.Write(',\n'.join(['  &%s' % f for f in generated_functions]))
545        out.Write('\n};\n\n')
546
547    out.Write('}  // namespace\n')
548    out.Write('\n')
549    for thunk_type, thunk_name in version_list:
550      thunk_decl = 'const %s* Get%s_Thunk() {\n' % (thunk_type, thunk_type)
551      if len(thunk_decl) > 80:
552        thunk_decl = 'const %s*\n    Get%s_Thunk() {\n' % (thunk_type,
553                                                           thunk_type)
554      out.Write(thunk_decl)
555      out.Write('  return &%s;\n' % thunk_name)
556      out.Write('}\n')
557      out.Write('\n')
558    out.Write('}  // namespace thunk\n')
559    out.Write('}  // namespace ppapi\n')
560
561
562tgen = TGen()
563
564
565def Main(args):
566  # Default invocation will verify the golden files are unchanged.
567  failed = 0
568  if not args:
569    args = ['--wnone', '--diff', '--test', '--thunkroot=.']
570
571  ParseOptions(args)
572
573  idldir = os.path.split(sys.argv[0])[0]
574  idldir = os.path.join(idldir, 'test_thunk', '*.idl')
575  filenames = glob.glob(idldir)
576  ast = ParseFiles(filenames)
577  if tgen.GenerateRange(ast, ['M13', 'M14', 'M15'], {}):
578    print "Golden file for M13-M15 failed."
579    failed = 1
580  else:
581    print "Golden file for M13-M15 passed."
582
583  return failed
584
585
586if __name__ == '__main__':
587  sys.exit(Main(sys.argv[1:]))
588