blob: fb43c7692b55a695da0e345df4e20fd807248776 [file] [log] [blame]
# Copyright 2014 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generate Cobalt bindings (.h and .cpp files).
Based on and borrows heavily from code_generator_v8.py which is used as part
of the bindings generation pipeline in Blink.
"""
import abc
from datetime import date
import os
import sys
import bootstrap_path # pylint: disable=unused-import
from cobalt.bindings import path_generator
from cobalt.bindings.contexts import ContextBuilder
from cobalt.bindings.name_conversion import get_interface_name
from code_generator import CodeGeneratorBase
from code_generator import jinja2
from idl_definitions import IdlTypedef
from idl_types import IdlSequenceType
from idl_types import IdlType
module_path, module_filename = os.path.split(os.path.realpath(__file__))
# Track the cobalt directory, so we can use it for building relative paths.
cobalt_dir = os.path.normpath(os.path.join(module_path, os.pardir))
SHARED_TEMPLATES_DIR = os.path.abspath(os.path.join(module_path, 'templates'))
def initialize_jinja_env(cache_dir, templates_dir):
"""Initialize the Jinja2 environment."""
assert os.path.isabs(templates_dir)
assert os.path.isabs(SHARED_TEMPLATES_DIR)
# Ensure that we are using the version of jinja that's checked in to
# third_party.
assert jinja2.__version__ == '2.7.1'
jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader([templates_dir, SHARED_TEMPLATES_DIR]),
extensions=['jinja2.ext.do'], # do statement
# Bytecode cache is not concurrency-safe unless pre-cached:
# if pre-cached this is read-only, but writing creates a race condition.
bytecode_cache=jinja2.FileSystemBytecodeCache(cache_dir),
keep_trailing_newline=True, # newline-terminate generated files
lstrip_blocks=True, # so can indent control flow tags
trim_blocks=True)
return jinja_env
def normalize_slashes(path):
if os.path.sep == '\\':
return path.replace('\\', '/')
else:
return path
def is_global_interface(interface):
return (('PrimaryGlobal' in interface.extended_attributes) or
('Global' in interface.extended_attributes))
def get_indexed_special_operation(interface, special):
special_operations = list(
operation for operation in interface.operations
if (special in operation.specials and operation.arguments and
str(operation.arguments[0].idl_type) == 'unsigned long'))
assert len(special_operations) <= 1, (
'Multiple indexed %ss defined on interface: %s' % (special,
interface.name))
return special_operations[0] if len(special_operations) else None
def get_indexed_property_getter(interface):
getter_operation = get_indexed_special_operation(interface, 'getter')
assert not getter_operation or len(getter_operation.arguments) == 1
return getter_operation
def get_indexed_property_setter(interface):
setter_operation = get_indexed_special_operation(interface, 'setter')
assert not setter_operation or len(setter_operation.arguments) == 2
return setter_operation
def get_indexed_property_deleter(interface):
deleter_operation = get_indexed_special_operation(interface, 'deleter')
assert not deleter_operation or len(deleter_operation.arguments) == 1
return deleter_operation
def get_named_special_operation(interface, special):
special_operations = list(
operation for operation in interface.operations
if (special in operation.specials and operation.arguments and
str(operation.arguments[0].idl_type) == 'DOMString'))
assert len(special_operations) <= 1, (
'Multiple named %ss defined on interface: %s' % (special, interface.name))
return special_operations[0] if len(special_operations) else None
def get_named_property_getter(interface):
getter_operation = get_named_special_operation(interface, 'getter')
assert not getter_operation or len(getter_operation.arguments) == 1
return getter_operation
def get_named_property_setter(interface):
setter_operation = get_named_special_operation(interface, 'setter')
assert not setter_operation or len(setter_operation.arguments) == 2
return setter_operation
def get_named_property_deleter(interface):
deleter_operation = get_named_special_operation(interface, 'deleter')
assert not deleter_operation or len(deleter_operation.arguments) == 1
return deleter_operation
def get_interface_type_names_from_idl_types(info_provider, idl_type_list):
for idl_type in idl_type_list:
if idl_type:
idl_type = idl_type.resolve_typedefs(info_provider.typedefs)
if isinstance(idl_type, IdlTypedef):
idl_type = idl_type.idl_type
if idl_type.is_interface_type:
yield get_interface_name(idl_type)
if idl_type.is_dictionary:
yield idl_type.name
elif idl_type.is_union_type:
for interface_name in get_interface_type_names_from_idl_types(
info_provider, idl_type.member_types):
yield interface_name
elif idl_type.is_enum:
yield idl_type.name
elif isinstance(idl_type, IdlSequenceType):
for interface_name in get_interface_type_names_from_idl_types(
info_provider, [idl_type.element_type]):
yield interface_name
def get_interface_type_names_from_typed_objects(info_provider,
typed_object_list):
for typed_object in typed_object_list:
for interface_name in get_interface_type_names_from_idl_types(
info_provider, [typed_object.idl_type]):
yield interface_name
if hasattr(typed_object, 'arguments'):
for interface_name in get_interface_type_names_from_typed_objects(
info_provider, typed_object.arguments):
yield interface_name
def split_unsupported_properties(context_list):
supported = []
unsupported = []
for context in context_list:
if context['unsupported']:
unsupported.append(context['idl_name'])
else:
supported.append(context)
return supported, unsupported
class CodeGeneratorCobalt(CodeGeneratorBase):
"""Abstract Base code generator class for Cobalt.
Concrete classes will provide an implementation for generating bindings for a
specific JavaScript engine implementation.
"""
__metaclass__ = abc.ABCMeta
def __init__(self, templates_dir, info_provider, cache_dir, output_dir):
super(CodeGeneratorCobalt, self).__init__(
'CodeGeneratorCobalt', info_provider, cache_dir, output_dir)
# CodeGeneratorBase inititalizes this with the v8 template path, so
# reinitialize it with cobalt's template path
# Whether the path is absolute or relative affects the cache file name. Use
# the absolute path to ensure that we use the same path as was used when the
# cache was prepopulated.
self.jinja_env = initialize_jinja_env(cache_dir,
os.path.abspath(templates_dir))
self.path_builder = path_generator.PathBuilder(
self.generated_file_prefix, self.info_provider, cobalt_dir, output_dir)
@abc.abstractproperty
def generated_file_prefix(self):
"""The prefix to prepend to all generated source files."""
pass
@abc.abstractproperty
def expression_generator(self):
"""An instance that implements the ExpressionGenerator class."""
pass
def render_template(self, template_filename, template_context):
template = self.jinja_env.get_template(template_filename)
template_context.update(self.common_context(template))
rendered_text = template.render(template_context)
return rendered_text
def generate_code(self, definitions, definition_name):
if definition_name in definitions.interfaces:
return self.generate_interface_code(
definitions, definition_name, definitions.interfaces[definition_name])
if definition_name in definitions.dictionaries:
return self.generate_dictionary_code(
definitions, definition_name,
definitions.dictionaries[definition_name])
if definition_name in definitions.enumerations:
return self.generate_enum_code(definitions, definition_name,
definitions.enumerations[definition_name])
raise ValueError('%s is not in IDL definitions' % definition_name)
def generate_interface_code(self, definitions, interface_name, interface):
interface_info = self.info_provider.interfaces_info[interface_name]
# Select appropriate Jinja template and contents function
if interface.is_callback:
header_template_filename = 'callback-interface.h.template'
cpp_template_filename = 'callback-interface.cc.template'
elif interface.is_partial:
raise NotImplementedError('Partial interfaces not implemented')
else:
header_template_filename = 'interface.h.template'
cpp_template_filename = 'interface.cc.template'
template_context = self.build_interface_context(interface, interface_info,
definitions)
header_text = self.render_template(header_template_filename,
template_context)
cc_text = self.render_template(cpp_template_filename, template_context)
header_path = self.path_builder.BindingsHeaderFullPath(interface_name)
cc_path = self.path_builder.BindingsImplementationPath(interface_name)
return (
(header_path, header_text),
(cc_path, cc_text),)
def generate_dictionary_code(self, definitions, dictionary_name, dictionary):
header_template_filename = 'dictionary.h.template'
conversion_template_filename = 'dictionary-conversion.cc.template'
implementation_context = self.build_dictionary_context(
dictionary, definitions, False)
conversion_context = self.build_dictionary_context(dictionary, definitions,
True)
header_text = self.render_template(header_template_filename,
implementation_context)
conversion_text = self.render_template(conversion_template_filename,
conversion_context)
header_path = self.path_builder.DictionaryHeaderFullPath(dictionary_name)
conversion_impl_path = (
self.path_builder.DictionaryConversionImplementationPath(
dictionary_name))
return (
(header_path, header_text),
(conversion_impl_path, conversion_text),)
def generate_enum_code(self, definitions, enumeration_name, enumeration):
header_template_filename = 'enumeration.h.template'
conversion_template_filename = 'enumeration-conversion.cc.template'
context_builder = ContextBuilder(self.info_provider)
context = context_builder.enumeration_context(enumeration)
context['components'] = self.path_builder.NamespaceComponents(
enumeration_name)
context['namespace'] = self.path_builder.Namespace(enumeration_name)
context['fully_qualified_name'] = self.path_builder.FullClassName(
enumeration_name)
context['enum_include'] = self.path_builder.EnumHeaderIncludePath(
enumeration_name)
header_text = self.render_template(header_template_filename, context)
conversion_text = self.render_template(conversion_template_filename,
context)
header_path = self.path_builder.EnumHeaderFullPath(enumeration_name)
conversion_impl_path = (
self.path_builder.EnumConversionImplementationFullPath(enumeration_name)
)
return (
(header_path, header_text),
(conversion_impl_path, conversion_text),)
def generate_conversion_code(self):
enumerations = list(self.info_provider.enumerations.keys())
dictionaries = list(self.info_provider.interfaces_info['dictionaries'])
includes = [
self.path_builder.DictionaryHeaderIncludePath(dictionary)
for dictionary in dictionaries
]
includes.extend([
self.path_builder.EnumHeaderIncludePath(enum) for enum in enumerations
])
context = {
'dictionaries': self.referenced_class_contexts(dictionaries, False),
'enumerations': self.referenced_class_contexts(enumerations, False),
'includes': includes,
}
header_template_filename = 'generated-types.h.template'
header_text = self.render_template(header_template_filename, context)
return self.path_builder.generated_conversion_header_path, header_text
def referenced_dictionary_context(self, dictionary_name, dictionary_info):
namespace = '::'.join(
self.path_builder.NamespaceComponents(dictionary_name))
return {
'fully_qualified_name':
'%s::%s' % (namespace, dictionary_name),
'include':
self.path_builder.DictionaryHeaderIncludePath(dictionary_name),
'conditional':
dictionary_info['conditional'],
'is_callback_interface':
False,
}
def referenced_enum_context(self, enum_name):
namespace = '::'.join(self.path_builder.NamespaceComponents(enum_name))
return {
'fully_qualified_name': '%s::%s' % (namespace, enum_name),
'include': self.path_builder.EnumHeaderIncludePath(enum_name),
'conditional': None,
'is_callback_interface': False,
}
def referenced_class_contexts(self,
interface_names,
include_bindings_class=True):
"""Returns a list of jinja contexts describing referenced C++ classes.
Args:
interface_names: A list of interfaces.
include_bindings_class: Include headers and classes uses only in bindings.
Returns:
list() of jinja contexts (python dicts) with information about C++ classes
related to the interfaces in |interface_names|. dict has the following
keys:
fully_qualified_name: Fully qualified name of the class.
include: Path to the header that defines the class.
conditional: Symbol on which this interface is conditional compiled.
is_callback_interface: True iff this is a callback interface.
"""
referenced_classes = []
# Iterate over it as a set to uniquify the list.
for interface_name in set(interface_names):
if interface_name in self.info_provider.enumerations:
# If this is an enumeration, get the enum context and continue.
referenced_classes.append(self.referenced_enum_context(interface_name))
continue
interface_info = self.info_provider.interfaces_info[interface_name]
if interface_info['is_dictionary']:
referenced_classes.append(
self.referenced_dictionary_context(interface_name, interface_info))
else:
namespace = '::'.join(
self.path_builder.NamespaceComponents(interface_name))
conditional = interface_info['conditional']
is_callback_interface = interface_name in IdlType.callback_interfaces
referenced_classes.append({
'fully_qualified_name':
'%s::%s' % (namespace, interface_name),
'include':
self.path_builder.ImplementationHeaderPath(interface_name),
'conditional':
conditional,
'is_callback_interface':
is_callback_interface,
})
if include_bindings_class:
referenced_classes.append({
'fully_qualified_name':
'%s::%s' % (namespace,
self.path_builder.BindingsClass(interface_name)),
'include':
self.path_builder.BindingsHeaderIncludePath(interface_name),
'conditional':
conditional,
'is_callback_interface':
is_callback_interface,
})
return referenced_classes
def common_context(self, template):
"""Shared stuff."""
# Make sure extension is .py, not .pyc or .pyo, so doesn't depend on caching
module_path_pyname = os.path.join(
module_path, os.path.splitext(module_filename)[0] + '.py')
# Ensure that posix forward slashes are used
context = {
'today':
date.today(),
'code_generator':
normalize_slashes(os.path.relpath(module_path_pyname, cobalt_dir)),
'template_path':
normalize_slashes(os.path.relpath(template.filename, cobalt_dir)),
'generated_conversion_include':
self.path_builder.generated_conversion_include_path,
}
return context
def build_dictionary_context(self, dictionary, definitions, for_conversion):
context_builder = ContextBuilder(self.info_provider)
context = {
'class_name':
dictionary.name,
'header_file':
self.path_builder.DictionaryHeaderIncludePath(dictionary.name),
'members': [
context_builder.get_dictionary_member_context(dictionary, member)
for member in dictionary.members
]
}
referenced_interface_names = set(
get_interface_type_names_from_typed_objects(self.info_provider,
dictionary.members))
if dictionary.parent:
referenced_interface_names.add(dictionary.parent)
parent_namespace = '::'.join(
self.path_builder.NamespaceComponents(dictionary.parent))
context['parent'] = '%s::%s' % (parent_namespace, dictionary.parent)
referenced_class_contexts = self.referenced_class_contexts(
referenced_interface_names, for_conversion)
context['includes'] = sorted(interface['include']
for interface in referenced_class_contexts)
context['forward_declarations'] = sorted(
referenced_class_contexts, key=lambda x: x['fully_qualified_name'])
context['components'] = self.path_builder.NamespaceComponents(
dictionary.name)
return context
def build_interface_context(self, interface, interface_info, definitions):
context_builder = ContextBuilder(self.info_provider)
context = {
# Parameters used for template rendering.
'today':
date.today(),
'binding_class':
self.path_builder.BindingsClass(interface.name),
'fully_qualified_binding_class':
self.path_builder.FullBindingsClassName(interface.name),
'header_file':
self.path_builder.BindingsHeaderIncludePath(interface.name),
'impl_class':
interface.name,
'fully_qualified_impl_class':
self.path_builder.FullClassName(interface.name),
'interface_name':
interface.name,
'is_global_interface':
is_global_interface(interface),
'has_interface_object': (
'NoInterfaceObject' not in interface.extended_attributes),
'conditional':
interface.extended_attributes.get('Conditional', None),
}
interfaces_info = self.info_provider.interfaces_info
if is_global_interface(interface):
# Global interface references all interfaces.
referenced_interface_names = set(
interface_name for interface_name in interfaces_info['all_interfaces']
if not interfaces_info[interface_name]['unsupported'] and
not interfaces_info[interface_name]['is_callback_interface'] and
not interfaces_info[interface_name]['is_dictionary'])
referenced_interface_names.update(IdlType.callback_interfaces)
else:
# Build the set of referenced interfaces from this interface's members.
referenced_interface_names = set(
get_interface_type_names_from_typed_objects(self.info_provider,
interface.attributes))
referenced_interface_names.update(
get_interface_type_names_from_typed_objects(self.info_provider,
interface.operations))
referenced_interface_names.update(
get_interface_type_names_from_typed_objects(self.info_provider,
interface.constructors))
referenced_interface_names.update(
get_interface_type_names_from_typed_objects(
self.info_provider, definitions.callback_functions.values()))
# Build the set of #includes in the header file. Try to keep this small
# to avoid circular dependency problems.
header_includes = set()
if interface.parent:
header_includes.add(
self.path_builder.BindingsHeaderIncludePath(interface.parent))
referenced_interface_names.add(interface.parent)
header_includes.add(
self.path_builder.ImplementationHeaderPath(interface.name))
attributes = [
context_builder.attribute_context(interface, attribute, definitions)
for attribute in interface.attributes
]
constructor = context_builder.get_constructor_context(
self.expression_generator, interface)
methods = context_builder.get_method_contexts(self.expression_generator,
interface)
constants = [
context_builder.constant_context(c) for c in interface.constants
]
# Get a list of all the unsupported property names, and remove the
# unsupported ones from their respective lists
attributes, unsupported_attribute_names = split_unsupported_properties(
attributes)
methods, unsupported_method_names = split_unsupported_properties(methods)
constants, unsupported_constant_names = split_unsupported_properties(
constants)
# Build a set of all interfaces referenced by this interface.
referenced_class_contexts = self.referenced_class_contexts(
referenced_interface_names)
all_interfaces = []
for interface_name in referenced_interface_names:
if (interface_name not in IdlType.callback_interfaces and
interface_name not in IdlType.enums and
not interfaces_info[interface_name]['unsupported'] and
not interfaces_info[interface_name]['is_dictionary']):
all_interfaces.append({
'name': interface_name,
'conditional': interfaces_info[interface_name]['conditional'],
})
context['implementation_includes'] = sorted(
(interface['include'] for interface in referenced_class_contexts))
context['header_includes'] = sorted(header_includes)
context['attributes'] = [a for a in attributes if not a['is_static']]
context['static_attributes'] = [a for a in attributes if a['is_static']]
context['constants'] = constants
context['constructor'] = constructor
context['named_constructor'] = interface.extended_attributes.get(
'NamedConstructor', None)
context['interface'] = interface
context['operations'] = [m for m in methods if not m['is_static']]
context['static_operations'] = [m for m in methods if m['is_static']]
context['unsupported_interface_properties'] = set(
unsupported_attribute_names + unsupported_method_names +
unsupported_constant_names)
context['unsupported_constructor_properties'] = set(
unsupported_constant_names)
if interface.parent:
context['parent_interface'] = self.path_builder.BindingsClass(
interface.parent)
context['is_exception_interface'] = interface.is_exception
context['forward_declarations'] = sorted(
referenced_class_contexts, key=lambda x: x['fully_qualified_name'])
context['all_interfaces'] = sorted(all_interfaces, key=lambda x: x['name'])
context['callback_functions'] = definitions.callback_functions.values()
context['enumerations'] = [
context_builder.enumeration_context(enumeration)
for enumeration in definitions.enumerations.values()
]
context['components'] = self.path_builder.NamespaceComponents(
interface.name)
context['stringifier'] = context_builder.stringifier_context(interface)
context['indexed_property_getter'] = context_builder.special_method_context(
interface, get_indexed_property_getter(interface))
context['indexed_property_setter'] = context_builder.special_method_context(
interface, get_indexed_property_setter(interface))
context[
'indexed_property_deleter'] = context_builder.special_method_context(
interface, get_indexed_property_deleter(interface))
context['named_property_getter'] = context_builder.special_method_context(
interface, get_named_property_getter(interface))
context['named_property_setter'] = context_builder.special_method_context(
interface, get_named_property_setter(interface))
context['named_property_deleter'] = context_builder.special_method_context(
interface, get_named_property_deleter(interface))
context['supports_indexed_properties'] = (
context['indexed_property_getter'] or
context['indexed_property_setter'])
context['supports_named_properties'] = (context['named_property_setter'] or
context['named_property_getter'] or
context['named_property_deleter'])
names = sorted([key for key in interfaces_info])
context['unique_id'] = names.index(interface.name)
return context
################################################################################
def main(argv):
# If file itself executed, cache templates
try:
cache_dir = argv[1]
templates_dir = argv[2]
dummy_filename = argv[3]
except IndexError:
print 'Usage: %s CACHE_DIR TEMPLATES_DIR DUMMY_FILENAME' % argv[0]
return 1
# Delete all jinja2 .cache files, since they will get regenerated anyways.
for filename in os.listdir(cache_dir):
if os.path.splitext(filename)[1] == '.cache':
# We expect that the only .cache files in this directory are for jinja2
# and they have a __jinja2_ prefix.
assert filename.startswith('__jinja2_')
os.remove(os.path.join(cache_dir, filename))
# Cache templates.
# Whether the path is absolute or relative affects the cache file name. Use
# the absolute path to ensure that the same path is used when we populate the
# cache here and when it's read during code generation.
jinja_env = initialize_jinja_env(cache_dir, os.path.abspath(templates_dir))
template_filenames = [
filename
for filename in os.listdir(templates_dir)
# Skip .svn, directories, etc.
if filename.endswith(('.template'))
]
assert template_filenames, 'Expected at least one template to be cached.'
shared_template_filenames = [
filename
for filename in os.listdir(SHARED_TEMPLATES_DIR)
# Skip .svn, directories, etc.
if filename.endswith(('.template'))
]
for template_filename in template_filenames + shared_template_filenames:
jinja_env.get_template(template_filename)
# Create a dummy file as output for the build system,
# since filenames of individual cache files are unpredictable and opaque
# (they are hashes of the template path, which varies based on environment)
with open(dummy_filename, 'w') as dummy_file:
pass # |open| creates or touches the file
if __name__ == '__main__':
sys.exit(main(sys.argv))