Code Index

In addition to generating the actual C++ code itself, the Coco Platform is also able to output a strucuted JSON representation of the generated code. This is intended toallow users to build their own code generators based on the output of the Coco generator. For example, using the JSON index it should be possible to:

  • Generate bindings to expose the generated C++ code to other languages (e.g. using Boost.Python or pybind).
  • Generate bindings to a C++ RPC framework.
  • Generate bindings to C++ code that follows a company-specific format.

The JSON code index is a structured format that shows the structure of the declarations in the generated code as an abstract syntax tree. As an example, given the Coco enum:

enum Range {
  Small;
  Large;
}

Coco would generate the following C++ code:

enum class Range : std::uint8_t {
  Small,
  Large,
}

For this, the JSON code index will contain:

{
  "kind": "EnumClassDecl",
  "fully_qualified_name": "Range",
  "name": "Range",
  "coco_position": {
    "fully_qualified_name": "Example.Range",
    "kind": "EnumerationDecl",
    "name": "Range"
  },
  "declarations": [
    {
      "coco_position": {
        "fully_qualified_name": "Example.Range.Large",
        "kind": "EnumerationCaseDecl",
        "name": "Large"
      },
      "fully_qualified_name": "Range::Large",
      "kind": "EnumConstantDecl",
      "name": "Large"
    },
    {
      "coco_position": {
        "fully_qualified_name": "Example.Range.Small",
        "kind": "EnumerationCaseDecl",
        "name": "Small"
      },
      "fully_qualified_name": "Range::Small",
      "kind": "EnumConstantDecl",
      "name": "Small"
    }
  ]
}

The above illustrates the essential features of the code index:

  • Declarations appear in a nested tree-like structure where a kind field can be used to distinguish what sort of declaration it is;
  • Declarations often (but not always) include a property called coco_position which relates the generated code back to the original Coco declarations. This also includes a structured representation of attributes, if any are applied, allowing for additional data to be passed from Coco to the other tools.

See also

Schemas: Gives full documentation for the JSON index, including the details of what properties each node has.

generate-cpp: Gives documentation on the command-line tooling which specifies how to generate code indexes.

Example: Generating Python Bindings

The following gives an example of how the code index can be used to generate Python bindings to enable the Coco-generated C++ code to be used from Python using either pybind or Boost.Python. The full source code is appended below.

The generator is structured as a recursive visitor where each C++ node kind is handled by a different method. The base visit method looks at node["kind"] in order to determine which visit method to call:

  def _visit(self, node: dict, context_var: str):
    method_name = "_visit_%s" % node["kind"]
    if not hasattr(self, method_name):
      # Unsupported node
      return
    getattr(self, method_name)(node, context_var)

This method ignores nodes for which there is no handler: this enables this code to be compatible with future code index formats.

The most straightforward case to handle is a namespace declaration. For this, the visitor just recursively invokes itself:

  def _visit_NamespaceDecl(self, node: dict, context_var: str):
    self._visit_all(node["declarations"], context_var)

The following function creates Python bindings to C++ enum class declarations:

  def _visit_EnumClassDecl(self, node: dict, context_var: str):
    """Handles a field of a c++ enum class declaration"""
    self._add_module_line('{enum_type}<{fqn}>({context_var}"{name}")'.format(
        enum_type=self._enum_type,
        name=node["name"],
        fqn=node["fully_qualified_name"],
        context_var=self._context_var_template.format(name=context_var),
    ))
    for constant in node["declarations"]:
      self._add_module_line('  .value("{name}", {fully_qualified_name})'.format(**constant))
    self._add_module_line(";")

This generates code such as the following:

pybind11::enum_<Range>(py_module, "Range")
  .value("Small", Range::Small)
  .value("Large", Range::Large

The full example can be found below.

#!/usr/bin/env python3
#
# MIT License
#
# Copyright (c) 2021 Cocotec Limited
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# SPDX-License-Identifier: MIT

import argparse
import json
import io
import sys
from typing import List


def is_kind(expected: str, node: dict):
  """Checks if the node's kind (from the JSON code index) matches the specified kind"""
  return node["kind"] == expected


def find_decl(decls: List[dict], name: str):
  """Finds the declaration with the given name from the list of declarations"""
  for decl in decls:
    if decl["name"] == name:
      return decl
  return None


def eval_pointer(pointer: str, doc: dict):
  """Evaluates a JSON pointer"""
  elements = pointer.split('/')
  if elements.pop(0) != '':
    raise Exception('Only asbolute pointers are supported')
  for element in elements:
    try:
      doc = doc[int(element)]
    except:
      doc = doc[element]
  return doc


def eval_json_ref(pointer: dict, doc: dict):
  """Evaluates a JSON reference"""
  return eval_pointer(pointer["$ref"], doc)


def add_parent_pointers(object, parent=None):
  """Adds 'parent' keys to each dictionary to point to the parent object."""
  if isinstance(object, dict):
    object["parent"] = parent
    for k, v in object.items():
      if k != "parent":
        add_parent_pointers(v, object)
  elif isinstance(object, list):
    for v in object:
      add_parent_pointers(v, parent)
  else:
    pass


class IndentingStream:
  """A python io-like stream that can indent blocks of text."""

  def __init__(self, out: io.TextIOBase):
    self.is_newline = True
    self.out = out
    self.current_indent = 0

  def indent(self, indent=2):
    self.current_indent += indent

  def dedent(self, indent=2):
    self.current_indent -= indent

  def write(self, s: str) -> int:
    if self.is_newline:
      self.out.write(' ' * self.current_indent)
    self.is_newline = s.endswith('\n')
    return self.out.write(s)


def has_flag(flag: str, node: dict):
  assert is_kind("MethodDecl", node)
  return flag in node["flags"]


class BaseBindingGenerator:
  """An abstract python binding generator that can be configured via subclassing.

  This generates bindings for a JSON index by iterating over all declarations, and
  generating suitable bindings for each C++ object encounteerd. It is constructed
  as a recursive visitor: the main method is _visit which dispatches to a different
  _visit_x method depending on the kind of the node. As an example, see
  _visit_EnumClassDecl which specifies how a C++ enum class generated from Coco
  should be mapped into python.
  """

  def __init__(self, module_name: str, module_context_var: str = "py_module"):
    self._module_name = module_name
    self._module_context_var = module_context_var

    self._current_document = None
    self._includes = []
    self._module_lines = []
    self._namespace_lines = []
    self._publicists = {}

    self._add_include("coco/stream_logger.h")

  def add_bindings_for(self, doc: dict):
    self._current_document = doc
    for f in doc["generated"]:
      self._add_include(f["include_path"])
      self._visit_all(f["declarations"], self._module_context_var)
    self._current_document = None

  def emit(self, out: io.TextIOBase):
    printer = IndentingStream(out)
    for include in self._includes:
      printer.write("#include \"%s\"\n" % include)
    printer.write("")
    printer.write("namespace {\n")
    printer.write("")
    self._emit_helpers(printer)
    for line in self._namespace_lines:
      printer.write(line)
      printer.write('\n')
    printer.write("} // (anonymous namespace)\n\n")
    printer.write("")
    self._emit_module_header(printer)
    printer.indent()
    for line in self._module_lines:
      printer.write(line)
      printer.write('\n')
    printer.dedent()
    printer.write("}\n")

  def _eval_json_ref(self, pointer: str):
    return eval_json_ref(pointer, self._current_document)

  def _visit(self, node: dict, context_var: str):
    method_name = "_visit_%s" % node["kind"]
    if not hasattr(self, method_name):
      # Unsupported node
      return
    getattr(self, method_name)(node, context_var)

  def _visit_all(self, nodes, context_var: str):
    for node in nodes:
      self._visit(node, context_var)

  def _visit_NamespaceDecl(self, node: dict, context_var: str):
    self._visit_all(node["declarations"], context_var)

  def _visit_ConstructorDecl(self, node: dict, context_var: str):
    self._add_module_line("{context_var}.def({init}<{arguments}>());".format(
        context_var=context_var,
        init=self._init_template,
        arguments=", ".join([p["declared_type"]["rendered"] for p in node["parameters"]]),
    ))

  def _visit_FieldDecl(self, node: dict, context_var: str):
    """Handles a field of a c++ struct/class"""
    readonly = not node["declared_type"]["rendered"].endswith("*")
    self._add_module_line('{context_var}.def_{method}("{name}", &{context}::{name});'.format(
        context_var=context_var,
        method="readonly" if readonly else "readwrite",
        context=self._ensure_public(node)["parent"]["fully_qualified_name"],
        name=node["name"]))

  def _visit_EnumClassDecl(self, node: dict, context_var: str):
    """Handles a field of a c++ enum class declaration"""
    self._add_module_line('{enum_type}<{fqn}>({context_var}"{name}")'.format(
        enum_type=self._enum_type,
        name=node["name"],
        fqn=node["fully_qualified_name"],
        context_var=self._context_var_template.format(name=context_var),
    ))
    for constant in node["declarations"]:
      self._add_module_line('  .value("{name}", {fully_qualified_name})'.format(**constant))
    self._add_module_line(";")

  def _visit_MethodDecl(self, node: dict, context_var: str):
    """Handles a method of a c++ struct/class"""
    public_context_name = self._ensure_public(node["parent"])["fully_qualified_name"]
    public_method_name = self._ensure_public(node)["fully_qualified_name"]
    arguments = [
        "\"%s\"" % node["name"],
    ]
    fn = "static_cast<{function_type}>(&{public_method_name})".format(
        function_type=("{return_type}(*)({argument_types})" if has_flag("static", node) else
                       "{return_type}({public_context_name}::*)({argument_types})").format(
                           return_type=node["return_type"]["rendered"],
                           argument_types=", ".join([p["declared_type"]["rendered"] for p in node["parameters"]]),
                           public_context_name=public_context_name),
        public_method_name=public_method_name,
    )
    arguments.append(self._wrap_method_call(node, fn))
    if node["return_type"]["rendered"].endswith("&"):
      # We assume that references are returning references to objects owend by the parent class
      arguments.append(self._reference_internal)
    arguments += self._extra_method_arguments(node)
    arguments += ['%s("%s")' % (self._kwarg_function, p["name"]) for p in node["parameters"]]

    self._add_module_line("{context_var}.{def_method}({arguments});".format(
        context_var=context_var,
        def_method=self._def_static if has_flag("static", node) else "def",
        arguments=", ".join(arguments)))

  def _visit_RecordDecl(self, node: dict, context_var: str):
    public_node = self._ensure_public(node)
    trampoline = None
    # If this class has virtual methods and thus could be subclassed from python, create a trampoline to handle this
    if self._could_be_subclassed(node):
      trampoline = self._create_trampoline(node)

    if node.get("specialisation_arguments", []):
      return

    class_var, holder_type = self._internal_begin_RecordDecl(node, context_var, public_node, trampoline)

    if "coco_position" in node and node["coco_position"]["kind"] == "ComponentDecl" and \
        node["coco_position"]["component_kind"] == "Encapsulating":
      # Add a helper to set a logger
      self._add_module_line('{class_var}.def("set_logger_stderr", &set_logger_cerr);'.format(
          class_var=class_var, name=node["fully_qualified_name"]))
      self._add_namespace_line(
          'void set_logger_cerr({name}& c) {{ c.set_logger_recursively(coco::StreamMachineLogger::cerr()); }}'.format(
              name=node["fully_qualified_name"],))

      # Add a helper to construct the system using python kwargs
      arguments_struct = find_decl(node["declarations"], "Arguments")
      args = [f for f in arguments_struct["declarations"] if is_kind("FieldDecl", f)]
      factory_name = "create_%s" % node["fully_qualified_name"].replace("::", "__")
      self._add_namespace_line("{holder_type} {factory_name}({arguments}) {{".format(
          factory_name=factory_name,
          holder_type=holder_type,
          arguments=", ".join(["%s %s" % (arg["declared_type"]["rendered"], arg["name"]) for arg in args])))
      self._add_namespace_line("%s args;" % arguments_struct["fully_qualified_name"])
      for arg in args:
        self._add_namespace_line("args.{name} = {name};".format(name=arg["name"]))
      self._add_namespace_line('return {holder_type}(new {name}(args));'.format(holder_type=holder_type,
                                                                                name=node["fully_qualified_name"]))
      self._add_namespace_line('}')

      self._add_module_line(
          self._kwarg_constructor_template.format(
              factory_name=factory_name,
              class_var=class_var,
              policies=", ".join(['%s<0, %s>()' % (self._keep_alive_init, i) for i in range(1, 1 + len(args))]),
              kwargs=", ".join(['%s("%s")' % (self._kwarg_function, arg["name"]) for arg in args])))

    # Create all nested declarations
    for child in node["declarations"]:
      self._visit(child, class_var)

    self._internal_end_RecordDecl(node)

  def _could_be_subclassed(self, node: dict):
    """Checks to see if there are any methods that can be overriden from python"""
    if node["is_final"]:
      # Can't be subclassed
      return False
    for child in node["declarations"]:
      if is_kind("MethodDecl", child) and has_flag("virtual", child) and not has_flag("final", child):
        return True
    return False

  def _create_trampoline(self, node: dict):
    self._add_trampoline_class_decl(node)
    # Handle constructors
    self._add_namespace_line("  using {name}::{name};".format(name=node["name"]))
    self._add_trampoline_overrides(node, node)
    self._add_namespace_line("};")

    return {
        "kind": "RecordDecl",
        "name": "%sTrampoline" % node["name"],
        "fully_qualified_name": "%sTrampoline" % node["name"],
    }

  def _add_trampoline_overrides(self, original: dict, node: dict):
    """Recursively creates all overrides that could be made from python, including in any base classes"""
    had_override = False

    for method in node["declarations"]:
      if not is_kind("MethodDecl", method) or not has_flag("virtual", method) or has_flag("final", method):
        continue

      had_override |= has_flag("override", method)

      self._add_namespace_line("  {return_type} {method_name}({parameters}) {flags} {{".format(
          return_type=method["return_type"]["rendered"],
          method_name=method["name"],
          parameters=", ".join([
              "%s %s" % (parameter["declared_type"]["rendered"], parameter["name"])
              for parameter in method["parameters"]
          ]),
          flags=" ".join(["override"])))
      self._add_trampoline_call(original, method)
      self._add_namespace_line("  }")

    if had_override:
      # Stop recursing and assume we have overriden all methods. This is only a heuristic.
      return False

    # Recursively override more methods
    for base in self._iterate_bases(node, True):
      self._add_trampoline_overrides(original, base)

  def _ensure_public(self, node: dict):
    if node.get("access_specifier", "public") == "protected" or node["parent"].get("access_specifier",
                                                                                   "public") == "protected":
      assert is_kind("RecordDecl", node['parent'])
      public_parent = self._publicist_for(node['parent'])
      return {
          'name': node['name'],
          'parent': public_parent,
          'fully_qualified_name': "%s::%s" % (public_parent['fully_qualified_name'], node['name']),
      }
    return node

  def _publicist_for(self, node: dict):
    assert is_kind("RecordDecl", node)

    result = self._publicists.get(node["fully_qualified_name"])
    if result != None:
      return result

    # Create
    publicist_base = self._ensure_public(node)
    publicist_name = "Publicist%s%s" % (node["name"], len(self._publicists))
    result = self._publicists[node["fully_qualified_name"]] = {
        "kind": "RecordDecl",
        "name": publicist_name,
        "fully_qualified_name": publicist_name,
        "publicist_base": publicist_base,
    }

    self._add_namespace_line("{record_kind} {publicist_name} : public {base_name} {{".format(
        publicist_name=publicist_name, base_name=publicist_base["fully_qualified_name"], **node))
    self._add_namespace_line("public:")
    public_decls = set()
    self._add_public_methods(publicist_base["fully_qualified_name"], node, public_decls)
    self._add_namespace_line("};")

    return result

  def _add_public_methods(self, base_name: str, node: dict, public_decls: set):
    for method in node["declarations"]:
      if method["kind"] in ["FieldDecl", "MethodDecl", "RecordDecl"] and not method["name"] in public_decls:
        public_decls.add(method["name"])
        self._add_namespace_line("  using {base_name}::{method_name};".format(base_name=base_name,
                                                                              method_name=method["name"]))
    for base in self._iterate_bases(node):
      self._add_public_methods(base_name, base, public_decls)

  def _iterate_bases(self, node: dict, transitive_bases=False):
    for base_class in node.get("base_classes", []):
      if "simple_type_reference" in base_class["type"]:
        yield self._eval_json_ref(base_class["type"]["simple_type_reference"])
      # elif not transitive_bases: # and base_class["type"]["rendered"] == "coco::BaseProvidedInterfacePort":
      elif transitive_bases and base_class["type"]["rendered"] == "coco::BaseProvidedInterfacePort":
        yield {
            "base_classes": [],
            "declarations": [{
                "access_specifier": "public",
                "flags": ["virtual", "pure_virtual"],
                "fully_qualified_name": "coco::BaseProvidedInterfacePort::register_client",
                "kind": "MethodDecl",
                "name": "register_client",
                "parameters": [],
                "return_type": {
                    "kind": "QualifiedType",
                    "rendered": "void"
                }
            }, {
                "access_specifier": "public",
                "flags": ["virtual", "pure_virtual"],
                "fully_qualified_name": "coco::BaseProvidedInterfacePort::unregister_client",
                "kind": "MethodDecl",
                "name": "unregister_client",
                "parameters": [],
                "return_type": {
                    "kind": "QualifiedType",
                    "rendered": "void"
                }
            }],
            "fully_qualified_name": "coco::BaseProvidedInterfacePort",
            "is_final": False,
            "kind": "RecordDecl",
            "name": "BaseProvidedInterfacePort",
            "record_kind": "class"
        }

  def _add_module_line(self, line: str):
    self._module_lines.append(line)

  def _add_namespace_line(self, line: str):
    self._namespace_lines.append(line)

  def _add_include(self, include: str):
    self._includes.append(include)


class BoostPythonGenerator(BaseBindingGenerator):
  """A binding generator for Boost.Python"""

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self._add_include("boost/python.hpp")
    self._enum_type = "boost::python::enum_"
    self._class_type = "boost::python::class_"
    self._context_var_template = ""
    self._def_static = "def"
    self._init_template = "boost::python::init"
    self._keep_alive_init = "boost::python::with_custodian_and_ward_postcall"
    self._kwarg_constructor_template = '{class_var}.def("create", &{factory_name}, ({policies}), ({kwargs})).staticmethod("create");'
    self._kwarg_function = "boost::python::arg"
    self._reference_internal = "boost::python::return_internal_reference<>()"

  def _emit_module_header(self, printer):
    printer.write("BOOST_PYTHON_MODULE({module_name}) {{\n".format(module_name=self._module_name))

  def _emit_helpers(self, printer):
    printer.write("""
class gil_scoped_acquire {
 public:
  gil_scoped_acquire() : thread_state_(PyGILState_Ensure()) {}
  ~gil_scoped_acquire() {
    PyGILState_Release(thread_state_);
  }
 private:
  PyGILState_STATE thread_state_;
};

class gil_scoped_release {
 public:
  gil_scoped_release() : thread_state_(PyEval_SaveThread()) { }
  ~gil_scoped_release() {
    PyEval_RestoreThread(thread_state_);
  }
 private:
  PyThreadState* thread_state_;
};

template <class T>
struct shared_ptr_no_gil_deleter : std::shared_ptr<T> {
  shared_ptr_no_gil_deleter(T* data) : std::shared_ptr<T>(data, [](T* ptr){
    gil_scoped_release release;
    delete ptr;
  }) {}
};

""")

  def _add_trampoline_class_decl(self, node: dict):
    self._add_namespace_line(
        "{record_kind} {name}Trampoline final : public {fully_qualified_name}, public boost::python::wrapper<{fully_qualified_name}> {{"
        .format(**node))
    self._add_namespace_line(" public:")
    self._add_namespace_line(
        "  {name}Trampoline(PyObject* py_object) {{ boost::python::detail::initialize_wrapper(py_object, this); }}".
        format(**node))

  def _add_trampoline_call(self, original: dict, method: dict):
    # Reacquire the GIL before calling into python
    self._add_namespace_line("    gil_scoped_acquire gil_acquire;")
    arguments = ", ".join([parameter["name"] for parameter in method["parameters"]])
    return_keyword = "return " if method["return_type"]["rendered"] != "void" else ""
    if has_flag("pure_virtual", method):
      self._add_namespace_line('    {return_keyword}this->get_override("{name}")({arguments});'.format(
          name=method["name"],
          arguments=arguments,
          return_keyword=return_keyword,
      ))
    else:
      self._add_namespace_line(
          '    if (boost::python::override function = this->get_override("{name}")) {{'.format(**method))
      self._add_namespace_line('      {return_keyword}function({arguments});'.format(
          name=method["name"],
          arguments=arguments,
          return_keyword=return_keyword,
      ))
      self._add_namespace_line("    } else {")
      self._add_namespace_line("      {return_keyword}{original}::{name}({arguments});".format(
          name=method["name"],
          original=original["fully_qualified_name"],
          arguments=arguments,
          return_keyword=return_keyword,
      ))
      self._add_namespace_line("    }")

  def _extra_method_arguments(self, node: dict):
    return []

  def _internal_begin_RecordDecl(self, node: dict, context_var: str, public_node: dict, trampoline: dict):
    class_arguments = [public_node["fully_qualified_name"]]
    # Handle inheritance
    bases = list(self._iterate_bases(node))
    if bases:
      class_arguments.append("boost::python::bases<%s>" % ", ".join([b["fully_qualified_name"] for b in bases]))

    # If this class has virtual methods and thus could be subclassed from python, create a trampoline to handle this
    holder_type = class_arguments[0]
    if trampoline:
      holder_type = trampoline["fully_qualified_name"]
    # Coco components can block in their destructors waiting for components to shutdown, so make sure that we release
    # Python's GIL
    if "coco_position" in node and is_kind("ComponentDecl", node["coco_position"]):
      holder_type = "shared_ptr_no_gil_deleter<%s>" % holder_type
    if holder_type != class_arguments[0]:
      class_arguments.append(holder_type)
    class_arguments.append("boost::noncopyable")

    class_initialisers = ['"%s"' % node["name"], "boost::python::no_init"]

    # Output class definition
    class_context_var = "%s_py" % node["name"]
    self._add_module_line("")
    self._add_module_line("// %s" % node["fully_qualified_name"])
    self._add_module_line("{")
    self._add_module_line("{class_type}<{class_arguments}> {name}({class_initialisers});".format(
        class_type=self._class_type,
        name=class_context_var,
        class_arguments=", ".join(class_arguments),
        class_initialisers=", ".join(class_initialisers)))
    self._add_module_line("boost::python::scope scope = %s;" % class_context_var)

    # Assume a default constructor if we don't have any other constructors
    if not any([is_kind("ConstructorDecl", child) for child in node["declarations"]]):
      self._add_module_line("{class_var}.def({init}<>());".format(class_var=class_context_var,
                                                                  init=self._init_template))

    return class_context_var, holder_type

  def _internal_end_RecordDecl(self, node: dict):
    self._add_module_line("}")

  def _wrap_method_call(self, node, fn):
    if has_flag("pure_virtual", node):
      fn = "boost::python::pure_virtual(%s)" % fn
    return fn


class PyBindGenerator(BaseBindingGenerator):
  """A binding generator for pybind11"""

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self._class_type = "pybind11::class_"
    self._enum_type = "pybind11::enum_"
    self._context_var_template = "{name}, "
    self._def_static = "def_static"
    self._init_template = "pybind11::init"
    self._keep_alive_init = "pybind11::keep_alive"
    self._kwarg_function = "pybind11::arg"
    self._kwarg_constructor_template = "{class_var}.def(pybind11::init(&{factory_name}), {policies}, {kwargs});"
    self._reference_internal = "pybind11::return_value_policy::reference_internal"
    self._add_include("pybind11/pybind11.h")

  def _emit_module_header(self, printer):
    printer.write("PYBIND11_MODULE({module_name}, {module_context_var}) {{\n".format(
        module_name=self._module_name,
        module_context_var=self._module_context_var,
    ))

  def _emit_helpers(self, printer):
    printer.write("""template <class T>
struct unique_ptr_no_gil_deleter {
  void operator()(T* ptr) {
    pybind11::gil_scoped_release gil_unlock;
    delete ptr;
  }
};
""")

  def _extra_method_arguments(self, node: dict):
    # In the multi-threaded runtime, coco methods can often block, so make sure that the GIL is acquired/released
    # around this method
    return ["pybind11::call_guard<pybind11::gil_scoped_release>()"]

  def _add_trampoline_class_decl(self, node: dict):
    self._add_namespace_line("{record_kind} {name}Trampoline final : public {fully_qualified_name} {{".format(**node))
    self._add_namespace_line(" public:")

  def _add_trampoline_call(self, original: dict, method: dict):
    self._add_namespace_line(
        "   PYBIND11_OVERLOAD{macro}({return_type}, {base_class}, {method_name}{divider}{arguments});".format(
            macro="_PURE" if has_flag("pure_virtual", method) else "",
            return_type=method["return_type"]["rendered"],
            base_class=original["fully_qualified_name"],
            method_name=method["name"],
            divider=", " if len(method["parameters"]) else "",
            arguments=", ".join([parameter["name"] for parameter in method["parameters"]]),
        ))

  def _internal_begin_RecordDecl(self, node: dict, context_var: str, public_node: dict, trampoline: dict):
    class_arguments = [public_node["fully_qualified_name"]]
    if trampoline:
      class_arguments.append(trampoline["fully_qualified_name"])
    # Coco components can block in their destructors waiting for components to shutdown, so make sure that we release
    # Python's GIL
    holder_type = "std::unique_ptr<{name}, unique_ptr_no_gil_deleter<{name}>>".format(name=class_arguments[0])
    if "coco_position" in node and is_kind("ComponentDecl", node["coco_position"]):
      class_arguments.append(
          "std::unique_ptr<{name}, unique_ptr_no_gil_deleter<{name}>>".format(name=class_arguments[0]))
    # Handle inheritance
    for base_class in self._iterate_bases(node):
      class_arguments.append(base_class["fully_qualified_name"])

    class_initialisers = [context_var, '"%s"' % node["name"]]

    # Output class definition
    class_context_var = "%s_py" % node["name"]
    self._add_module_line("")
    self._add_module_line("// %s" % node["fully_qualified_name"])
    self._add_module_line("{class_type}<{class_arguments}> {name}({class_initialisers});".format(
        class_type=self._class_type,
        name=class_context_var,
        class_arguments=", ".join(class_arguments),
        class_initialisers=", ".join(class_initialisers)))

    # Assume a default constructor if we don't have any other constructors
    if not any([is_kind("ConstructorDecl", child) for child in node["declarations"]]):
      self._add_module_line("{class_var}.def({init}<>());".format(class_var=class_context_var,
                                                                  init=self._init_template))

    return class_context_var, holder_type

  def _wrap_method_call(self, node, fn):
    return fn

  def _internal_end_RecordDecl(self, node: dict):
    pass


def main(args):
  parser = argparse.ArgumentParser()
  parser.add_argument('-o', '--out', help='output path, or - for stdout', default="-")
  parser.add_argument('input', default=["-"], nargs='+')
  parser.add_argument('--module-name', required=True)
  parser.add_argument('--type', required=True, choices=["boost", "pybind"])
  args = parser.parse_args(args)

  # Load all documents
  docs = []
  for input in args.input:
    if input == "-":
      docs.append(json.load(sys.stdin))
    else:
      with open(input, "r", encoding="utf-8") as f:
        docs.append(json.load(f))

  for doc in docs:
    add_parent_pointers(doc)

  if args.type == "pybind":
    generator = PyBindGenerator(module_name=args.module_name)
  else:
    generator = BoostPythonGenerator(module_name=args.module_name)
  for doc in docs:
    generator.add_bindings_for(doc)

  if args.out == "-":
    generator.emit(sys.stdout)
  else:
    with open(args.out, "w", encoding="utf-8") as f:
      generator.emit(f)


if __name__ == "__main__":
  main(sys.argv[1:])