blob: 2aa694a612aa5ad7a8cdd34aafdbca8335b2f6f9 [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
# Add blink's binding scripts to the path, so we can import
module_path, module_filename = os.path.split(os.path.realpath(__file__))
blink_script_dir = os.path.normpath(os.path.join(
module_path, os.pardir, os.pardir, 'third_party', 'blink', 'Source'))
sys.path.append(blink_script_dir)
from bindings.scripts.code_generator_v8 import CodeGeneratorBase # pylint: disable=g-import-not-at-top
from bindings.scripts.idl_types import IdlType
from bindings.scripts.idl_types import inherits_interface
import contexts
import jinja2
from name_conversion import convert_to_cobalt_name
from name_conversion import get_interface_name
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)
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 get_implementation_header_path(interface_info):
"""Get the path to the implementation header file.
The interface file must be under cobalt/...
Args:
interface_info: (dict) Contains the information about an interface
defined in an IDL, as generated by blink's
compute_interfaces_info_individual.py script
Returns:
(str) The path to the corresponding implementation header file, starting
with the 'cobalt' directory.
"""
implementation_header_absdir, filename = os.path.split(interface_info[
'full_path'])
implementation_header_reldir = os.path.relpath(implementation_header_absdir,
cobalt_dir)
assert not os.path.isabs(implementation_header_reldir)
assert os.pardir not in implementation_header_reldir.split(os.sep)
root, _ = os.path.splitext(filename)
header_filename = convert_to_cobalt_name(root) + '.h'
return normalize_slashes(os.path.join('cobalt', implementation_header_reldir,
header_filename))
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(idl_type_list):
for idl_type in idl_type_list:
if idl_type:
if idl_type.is_interface_type:
yield get_interface_name(idl_type)
elif idl_type.is_union_type:
for interface_name in get_interface_type_names_from_idl_types(
idl_type.member_types):
yield interface_name
def get_interface_type_names_from_typed_objects(typed_object_list):
for typed_object in typed_object_list:
for interface_name in get_interface_type_names_from_idl_types(
[typed_object.idl_type]):
yield interface_name
if hasattr(typed_object, 'arguments'):
for interface_name in get_interface_type_names_from_typed_objects(
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, interfaces_info, templates_dir, cache_dir, output_dir):
CodeGeneratorBase.__init__(self, interfaces_info, 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))
@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 wrapper_class_from_interface_name(self, interface_name):
return self.generated_file_prefix + interface_name
def wrapper_header_from_interface_name(self, interface_name):
attribute_wrapper_class = self.wrapper_class_from_interface_name(
interface_name)
return normalize_slashes(attribute_wrapper_class + '.h')
def output_paths(self, class_name):
"""Construct the filenames for a generated source file.
Construct the header and cc source file paths based on the
name of the generated class.
Args:
class_name: A string that is the name of the interface as defined
in the IDL
Returns:
(str, str): A tuple of two strings representing the full path of
the (.h, .cpp) file that will be generated for this interface
"""
header_path = os.path.join(self.output_dir, class_name + '.h')
cc_path = os.path.join(self.output_dir, class_name + '.cc')
return header_path, cc_path
def render_template(self, header_template_filename, cc_template_filename,
template_context):
header_template = self.jinja_env.get_template(header_template_filename)
cc_template = self.jinja_env.get_template(cc_template_filename)
# 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
template_context['code_generator'] = normalize_slashes(os.path.relpath(
module_path_pyname, cobalt_dir))
template_context['header_template_path'] = normalize_slashes(
os.path.relpath(header_template.filename, cobalt_dir))
template_context['cc_template_path'] = normalize_slashes(os.path.relpath(
cc_template.filename, cobalt_dir))
header_text = header_template.render(template_context)
cc_text = cc_template.render(template_context)
return header_text, cc_text
def generate_code_internal(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])
raise ValueError('%s is not in IDL definitions' % definition_name)
def generate_interface_code(self, definitions, interface_name, interface):
interface_info = self.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, cc_text = self.render_template(header_template_filename,
cpp_template_filename,
template_context)
header_path, cc_path = self.output_paths(template_context['binding_class'])
return ((header_path, header_text),
(cc_path, cc_text),)
def generate_dictionary_code(self, definitions, dictionary_name, dictionary):
raise NotImplementedError('Dictionaries not implemented')
def get_interface_components(self, idl_type_name):
"""Get the interface's namespace as a list of namespace components."""
idl_filename = self.interfaces_info[idl_type_name].get('full_path')
# Get the IDL filename relative to the cobalt directory, and split the
# directory to get the list of namespace components.
idl_filename = os.path.relpath(idl_filename, cobalt_dir)
components = os.path.dirname(idl_filename).split(os.sep)
return components
def referenced_interface_contexts(self, interface_name):
"""Yields information for including and declaring this interface's classes.
Args:
interface_name: The name of the interface.
Yields:
dict with 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.
"""
conditional = self.interfaces_info[interface_name]['conditional']
namespace = '::'.join(self.get_interface_components(interface_name))
full_name = 'cobalt::%s::%s' % (namespace, interface_name)
# Information about the Cobalt implementation class.
yield {
'fully_qualified_name': full_name,
'include': get_implementation_header_path(self.interfaces_info[
interface_name]),
'conditional': conditional,
'is_callback_interface': interface_name in IdlType.callback_interfaces,
}
yield {
'fully_qualified_name': 'cobalt::%s::%s' % (
namespace, self.wrapper_class_from_interface_name(interface_name)),
'include': self.wrapper_header_from_interface_name(interface_name),
'conditional': conditional,
'is_callback_interface': interface_name in IdlType.callback_interfaces,
}
def build_interface_context(self, interface, interface_info, definitions):
wrapper_class_name = self.wrapper_class_from_interface_name(interface.name)
header, _ = self.output_paths(wrapper_class_name)
generated_header_relative_path = os.path.relpath(header, self.output_dir)
assert not os.path.isabs(generated_header_relative_path)
assert os.pardir not in generated_header_relative_path.split(os.sep)
interface_components = (
self.get_interface_components(interface.idl_type.name))
context = {
# Parameters used for template rendering.
'today': date.today(),
'binding_class': wrapper_class_name,
'fully_qualified_binding_class': (
'::'.join(interface_components + [wrapper_class_name])),
'header_file': normalize_slashes(generated_header_relative_path),
'impl_class': interface.name,
'fully_qualified_impl_class': (
'::'.join(interface_components + [interface.name])),
'interface_name': interface.name,
'is_event_interface': inherits_interface(interface.name, 'Event'),
'is_global_interface': is_global_interface(interface),
'has_interface_object':
('NoInterfaceObject' not in interface.extended_attributes),
'conditional': interface.extended_attributes.get('Conditional', None),
'add_opaque_roots': interface.extended_attributes.get('AddOpaqueRoots',
None),
'get_opaque_root': interface.extended_attributes.get('GetOpaqueRoot',
None),
}
if is_global_interface(interface):
# Global interface references all interfaces.
referenced_interface_names = set(
interface_name
for interface_name in self.interfaces_info['all_interfaces']
if not self.interfaces_info[interface_name]['unsupported'] and
not self.interfaces_info[interface_name]['is_callback_interface'])
referenced_interface_names.update(IdlType.callback_interfaces)
else:
# Build the set of referenced interfaces from this interface's members.
referenced_interface_names = set()
referenced_interface_names.update(
get_interface_type_names_from_typed_objects(interface.attributes))
referenced_interface_names.update(
get_interface_type_names_from_typed_objects(interface.operations))
referenced_interface_names.update(
get_interface_type_names_from_typed_objects(interface.constructors))
referenced_interface_names.update(
get_interface_type_names_from_typed_objects(
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.wrapper_header_from_interface_name(
interface.parent))
header_includes.add(get_implementation_header_path(interface_info))
attributes = [contexts.attribute_context(interface, attribute, definitions)
for attribute in interface.attributes]
constructor = contexts.get_constructor_context(self.expression_generator,
interface)
methods = contexts.get_method_contexts(self.expression_generator, interface)
constants = [contexts.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_interfaces = {}
for interface_name in referenced_interface_names:
# Get information for classes related to this interface.
for referenced_interface_context in self.referenced_interface_contexts(
interface_name):
# Store it in the dictionary using fully_qualified_name as the key so
# we can avoid duplicates
key = referenced_interface_context['fully_qualified_name']
referenced_interfaces[key] = referenced_interface_context
# Now that we have all the referenced interfaces at most once, just get
# the values(), since the key is duplicated there and we no longer need
# to lookup by name.
referenced_interfaces = referenced_interfaces.values()
all_interfaces = []
for interface_name in referenced_interface_names:
if interface_name not in IdlType.callback_interfaces and not (
self.interfaces_info[interface_name]['unsupported']):
all_interfaces.append({
'name': interface_name,
'conditional': self.interfaces_info[interface_name]['conditional'],
})
context['implementation_includes'] = sorted(
(interface['include'] for interface in referenced_interfaces))
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'] = (
'::'.join(self.get_interface_components(interface.parent)) + '::%s' %
self.wrapper_class_from_interface_name(interface.parent))
context['is_exception_interface'] = interface.is_exception
context['components'] = self.get_interface_components(
interface.idl_type.name)
context['forward_declarations'] = sorted(
referenced_interfaces,
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'] = [
contexts.enumeration_context(enumeration)
for enumeration in definitions.enumerations.values()
]
context['stringifier'] = contexts.stringifier_context(interface)
context['indexed_property_getter'] = contexts.special_method_context(
interface, get_indexed_property_getter(interface))
context['indexed_property_setter'] = contexts.special_method_context(
interface, get_indexed_property_setter(interface))
context['indexed_property_deleter'] = contexts.special_method_context(
interface, get_indexed_property_deleter(interface))
context['named_property_getter'] = contexts.special_method_context(
interface, get_named_property_getter(interface))
context['named_property_setter'] = contexts.special_method_context(
interface, get_named_property_setter(interface))
context['named_property_deleter'] = contexts.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'])
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
# 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))