Wrapping a subset of a very large libraryΒΆ

Sometimes, for a very large library, only a subset of available C++ components is useful for end-users. Wrapping such libraries therefore requires AutoWIG to be able to consider only a subset of the C++ components during the Generate step. The Clang library is a complete C/C++ compiler. Clang is a great tool, but its stable Python interface (i.e. libclang) is lacking some useful features that are needed by AutoWIG. In particular, class template specializations are not available in the abstract syntax tree. Fortunately, most of the classes that would be needed during the traversal of the C++ abstract syntax tree are not template specializations. We therefore proposed to bootstrap the Clang Python bindings using the libclang parser of AutoWIG. This new Clang Python interface is called PyClangLite and is able to parse class template specializations. As for libclang, this interface is proposed only for a subset of the Clang library sufficient enough for proposing the new pyclanglite parser.

This repository already has wrappers, we therefore need to remove them.

In [ ]:
!git clone https://github.com/StatisKit/ClangLite ClangLite
!git -C ClangLite checkout a13322e37683012ca346595e88abc48ac591112c
In [ ]:
from path import Path
import shutil
srcdir = Path('ClangLite')/'src'/'py'
for wrapper in srcdir.walkfiles('*.cpp'):
    wrapper.unlink()
for wrapper in srcdir.walkfiles('*.h'):
    wrapper.unlink()
wrapper = srcdir/'clanglite'/'_clanglite.py'
if wrapper.exists():
    wrapper.unlink()
blddir = srcdir.parent.parent/'build'
if blddir.exists():
    shutil.rmtree(srcdir.parent.parent/'build')

In addition to the Clang libraries, the ClangLite library is needed in order to have access to some functionalities. The tool.h header of this ClangLite library includes all necessary Clang headers. This library is installed using the SCons cpp target.

In [ ]:
!conda remove libclanglite -y
!conda build ClangLite/conda/libclanglite -c statiskit -c conda-forge
!conda install -y libclanglite --use-local -c statiskit -c conda-forge

Once these preliminaries done, we can proceed to the actual generation of wrappers for the Clang library. For this, we import AutoWIG and create an empty Abstract Semantic Graph (ASG).

In [ ]:
import autowig
asg = autowig.AbstractSemanticGraph()

We then parse the tool.h header of the ClangLite library with relevant compilation flags.

In [ ]:
%%time
import sys
prefix = Path(sys.prefix).abspath()
autowig.parser.plugin = 'libclang'
asg = autowig.parser(asg, [prefix/'include'/'clanglite'/'tool.h'],
               flags = ['-x', 'c++', '-std=c++11',
                        '-D__STDC_LIMIT_MACROS',
                        '-D__STDC_CONSTANT_MACROS',
                        '-I' + str((prefix/'include').abspath())],
               libpath = prefix/'lib'/'libclang.so',
               bootstrap = False,
               silent = True)

Since most of AutoWIG guidelines are respected in the Clang library, the default controller implementation could be suitable. Nevertheless, we need to force some C++ components to be wrapped or not. We therefore implements a new controller.

In [ ]:
def clanglite_controller(asg):

    for node in asg['::boost::python'].classes(nested = True):
        node.is_copyable = True

    for node in asg.classes():
        node.boost_python_export = False
    for node in asg.functions(free=True):
        node.boost_python_export = False
    for node in asg.variables(free = True):
        node.boost_python_export = False
    for node in asg.enumerations():
        node.boost_python_export = False
    for node in asg.enumerators():
        if node.parent.boost_python_export:
            node.boost_python_export = False
    for node in asg.typedefs():
        node.boost_python_export = False

    from autowig.default_controller import refactoring
    asg = refactoring(asg)

    if autowig.parser.plugin == 'libclang':
        for fct in asg.functions(free=False):
            asg._nodes[fct._node]['_is_virtual'] = False
            asg._nodes[fct._node]['_is_pure'] = False
        asg['class ::clang::QualType'].is_abstract = False
        asg['class ::clang::QualType'].is_copyable = True
        asg['class ::llvm::StringRef'].is_abstract = False
        asg['class ::llvm::StringRef'].is_copyable = True
        asg['class ::clang::FileID'].is_abstract = False
        asg['class ::clang::FileID'].is_copyable = True
        asg['class ::clang::SourceLocation'].is_abstract = False
        asg['class ::clang::SourceLocation'].is_copyable = True
        asg['class ::clang::TemplateArgument'].is_abstract = False
        asg['class ::clang::TemplateArgument'].is_copyable = True
        for cls in ['::clang::FriendDecl', '::clang::CapturedDecl', '::clang::OMPThreadPrivateDecl',
                    '::clang::NonTypeTemplateParmDecl', '::clang::TemplateArgumentList', '::clang::ImportDecl',
                    '::clang::TemplateTemplateParmDecl', '::clang::CapturedDecl', '::clang::OMPThreadPrivateDecl',
                    '::clang::NonTypeTemplateParmDecl', '::clang::TemplateArgumentList', '::clang::ImportDecl',
                    '::clang::TemplateTemplateParmDecl']:
            asg['class ' + cls].is_abstract = False

    asg['class ::boost::python::api::object'].boost_python_export = True
    asg['class ::boost::python::list'].boost_python_export = True
    asg['class ::boost::python::str'].boost_python_export = True

    subset = []
    classes = [asg['class ::clang::QualType'],
               asg['class ::clang::Type'],
               asg['class ::clang::Decl']]
    subset += classes
    for cls in classes:
        subset += cls.subclasses(recursive=True)
    for cls in subset:
        if not cls.globalname.strip('class ') in ['::clang::QualType',
                                                  '::llvm::StringRef',
                                                  '::clang::FileID',
                                                  '::clang::SourceLocation',
                                                  '::clang::TemplateArgument',
                                                  '::clang::FriendDecl',
                                                  '::clang::CapturedDecl',
                                                  '::clang::OMPThreadPrivateDecl',
                                                  '::clang::NonTypeTemplateParmDecl',
                                                  '::clang::TemplateArgumentList',
                                                  '::clang::ImportDecl',
                                                  '::clang::TemplateTemplateParmDecl']:
            cls.is_copyable = False
        else:
            cls.is_copyable = True
    subset.append(asg['class ::llvm::StringRef'])

    subset.append(asg['class ::clang::ASTUnit'])
    subset.append(asg['class ::clang::ASTContext'])
    subset.append(asg['class ::clang::SourceManager'])
    subset.append(asg['class ::clang::FileID'])

    subset.append(asg['class ::clang::SourceLocation'])

    subset.append(asg['class ::clang::CXXBaseSpecifier'])
    subset.append(asg['class ::clang::DeclContext'])
    subset.append(asg['class ::clang::TemplateArgument'])

    subset.append(asg['class ::clang::TemplateArgumentList'])
    subset.append(asg['enum ::clang::Type::TypeClass'])
    subset.append(asg['enum ::clang::AccessSpecifier'])
    subset.append(asg['enum ::clang::LinkageSpecDecl::LanguageIDs'])
    subset.append(asg['enum ::clang::BuiltinType::Kind'])
    subset.append(asg['enum ::clang::TemplateArgument::ArgKind'])
    subset.append(asg['enum ::clang::Decl::Kind'])
    # subset.extend(asg['::boost::python'].classes(nested = True))
    # subset.extend(asg['::boost::python'].enumerations(nested = True))
    subset.extend(asg.nodes('::clanglite::build_ast_from_code_with_args'))

    for node in subset:
        node.boost_python_export = True

    for fct in asg['::clanglite'].functions():
        if not fct.localname == 'build_ast_from_code_with_args':
            fct.parent = fct.parameters[0].qualified_type.desugared_type.unqualified_type
        fct.boost_python_export = True

    for mtd in asg['class ::clang::ASTContext'].methods(pattern='.*getSourceManager.*'):
        if mtd.return_type.globalname == 'class ::clang::SourceManager &':
                mtd.boost_python_export = True
                break

    if autowig.parser.plugin == 'libclang':
        for node in (asg.functions(pattern='.*(llvm|clang).*_(begin|end)')
                     + asg.functions(pattern='::clang::CXXRecordDecl::getCaptureFields')
                     + asg.functions(pattern='.*(llvm|clang).*getNameAsString')
                     + asg.nodes('::clang::NamedDecl::getQualifiedNameAsString')
                     + asg.functions(pattern='.*::clang::ObjCProtocolDecl')
                     + asg.nodes('::clang::ObjCProtocolDecl::collectInheritedProtocolProperties')
                     + asg.nodes('::clang::ASTUnit::LoadFromASTFile')
                     + asg.nodes('::clang::ASTUnit::getCachedCompletionTypes')
                     + asg.nodes('::clang::ASTUnit::getBufferForFile')
                     + asg.nodes('::clang::CXXRecordDecl::getCaptureFields')
                     + asg.nodes('::clang::ASTContext::SectionInfos')
                     + asg.nodes('::clang::ASTContext::getAllocator')
                     + asg.nodes('::clang::ASTContext::getObjCEncoding.*')
                     + asg.nodes('::clang::ASTContext::getAllocator')
                     + asg.nodes('::clang::QualType::getAsString')
                     + asg.nodes('::clang::SourceLocation::printToString')
                     + asg['class ::llvm::StringRef'].methods()):
            node.boost_python_export = False

    if autowig.parser.plugin == 'clanglite':
        for mtd in asg['class ::clang::Decl'].methods():
            if mtd.localname == 'hasAttr':
                mtd.boost_python_export = False

    import sys
    from path import path
    for header in (path(sys.prefix)/'include'/'clang').walkfiles('*.h'):
        asg[header.abspath()].is_external_dependency = False

    return asg

This controller is then dynamically registered and used on the ASG.

In [ ]:
%%time
autowig.controller['clanglite'] = clanglite_controller
autowig.controller.plugin = 'clanglite'
asg = autowig.controller(asg)

In order to wrap a subset of the Clang library, we need to select the boost_python_internal generator implementation.

In [ ]:
%%time
autowig.generator.plugin = 'boost_python_pattern'
wrappers = autowig.generator(asg,
                  module = srcdir/'_clanglite.cpp',
                  decorator = srcdir/'clanglite'/'_clanglite.py',
                  closure = False)

The wrappers are only generated in-memory. It is therefore needed to write them on the disk to complete the process.

In [ ]:
%%time
wrappers.write()

Here is an example of the generated wrappers. We here present the wrappers for the clang::Decl class.

In [ ]:
!pygmentize ClangLite/src/py/wrapper_a6aedb4654a55a40aeecf4b1dc5fcc98.cpp

Once the wrappers are written on the disk, the bingings must be compiled and installed. This can be done using the SCons py target.

In [ ]:
!conda build ClangLite/conda/python-clanglite -c statiskit -c conda-forge
!conda install -y python-clanglite --use-local -c statiskit -c conda-forge
In [ ]:
import autowig
from clanglite.autowig_parser import autowig_parser
autowig.parser['clanglite'] = autowig_parser
autowig.parser.plugin = 'clanglite'
from path import Path
import sys

for wrapper in srcdir.walkfiles('*.cpp'):
    wrapper.unlink()
for wrapper in srcdir.walkfiles('*.h'):
    wrapper.unlink()
wrapper = srcdir/'clanglite'/'_clanglite.py'
if wrapper.exists():
    wrapper.unlink()

prefix = Path(sys.prefix).abspath()

asgbis = autowig.AbstractSemanticGraph()

asgbis = autowig.parser(asgbis, [prefix/'include'/'clanglite'/'tool.h'],
               flags = ['-x', 'c++', '-std=c++11',
                        '-D__STDC_CONSTANT_MACROS',
                        '-D__STDC_FORMAT_MACROS',
                        '-D__STDC_LIMIT_MACROS',
                        '-I' + str((prefix/'include').abspath()),
                        '-I' + str((prefix/'include'/'python2.7').abspath())],
               bootstrap = False,
               silent = True)

autowig.controller['clanglite'] = clanglite_controller
autowig.controller.plugin = 'clanglite'
asgbis = autowig.controller(asgbis)

autowig.generator.plugin = 'boost_python_pattern'
wrappers = autowig.generator(asgbis,
                  module = srcdir/'_clanglite.cpp',
                  decorator = srcdir/'clanglite'/'_clanglite.py',
                  closure = False)

wrappers.write()
In [ ]:
!conda remove python-clanglite -y
!conda build ClangLite/conda/python-clanglite -c statiskit -c conda-forge
!conda install -y python-clanglite --use-local -c statiskit -c conda-forge