diff --git a/CMakeLists.txt b/CMakeLists.txt index 84bf5c765..80f81626f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ option(NATS_BUILD_WITH_TLS "Build with TLS support" ON) option(NATS_BUILD_TLS_FORCE_HOST_VERIFY "Forces hostname verification" ON) option(NATS_BUILD_TLS_USE_OPENSSL_1_1_API "Build for OpenSSL 1.1+" OFF) option(NATS_BUILD_USE_SODIUM "Build using libsodium library" OFF) +option(NATS_BUILD_CPP_BINDINGS "Build binding for C++" OFF) option(NATS_BUILD_EXAMPLES "Build examples" ON) option(NATS_BUILD_LIBUV_EXAMPLE "Build libuv examples" OFF) option(NATS_BUILD_LIBEVENT_EXAMPLE "Build libevent examples" OFF) @@ -51,6 +52,10 @@ if(NATS_BUILD_WITH_TLS_CLIENT_METHOD) set(NATS_BUILD_TLS_USE_OPENSSL_1_1_API ON) endif(NATS_BUILD_WITH_TLS_CLIENT_METHOD) +if(NATS_BUILD_CPP_BINDINGS) + find_package(Python3 REQUIRED) +endif(NATS_BUILD_CPP_BINDINGS) + set(LIBUV_DIR "/usr/local/" CACHE PATH "Libuv install directory") set(LIBEVENT_DIR "/usr/local/" CACHE PATH "Libevent install directory") @@ -259,6 +264,9 @@ add_subdirectory(examples/getstarted) if(NATS_BUILD_STREAMING) add_subdirectory(examples/stan) endif() +if(NATS_BUILD_CPP_BINDINGS) + add_subdirectory(examples/cpp) +endif() add_subdirectory(test) add_subdirectory(test/dylib) #---------------------------- diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt new file mode 100644 index 000000000..d566182a4 --- /dev/null +++ b/examples/cpp/CMakeLists.txt @@ -0,0 +1,34 @@ +if(NOT NATS_BUILD_EXAMPLES) + return() +endif() + +# We need this directory to build the examples +include_directories(${PROJECT_SOURCE_DIR}/src) +include_directories(${PROJECT_SOURCE_DIR}/examples/cpp) +include_directories(${CMAKE_BINARY_DIR}/src) + +# Get all the .cpp files in the examples directory +file(GLOB EXAMPLES_SOURCES RELATIVE ${PROJECT_SOURCE_DIR}/examples/cpp *.cpp) + +if(NOT NATS_BUILD_STATIC_EXAMPLES) + add_definitions(-Dnats_IMPORTS) +endif() + +# For each file... +foreach(examples_src ${EXAMPLES_SOURCES}) + + # Remove the suffix so that it becomes the executable name + string(REPLACE ".cpp" "" examplename ${examples_src}) + set(exampleexe "nats-cpp-${examplename}") + + # Build the executable + add_executable(${exampleexe} ${PROJECT_SOURCE_DIR}/examples/cpp/${examples_src}) + + # Link + if(NATS_BUILD_STATIC_EXAMPLES) + target_link_libraries(${exampleexe} nats_static ${NATS_EXTRA_LIB}) + else() + target_link_libraries(${exampleexe} nats ${NATS_EXTRA_LIB}) + endif() + +endforeach() diff --git a/examples/cpp/subscriber.cpp b/examples/cpp/subscriber.cpp new file mode 100644 index 000000000..5cfdc9f12 --- /dev/null +++ b/examples/cpp/subscriber.cpp @@ -0,0 +1,65 @@ +// Copyright 2015-2020 The NATS Authors +// 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. + +#include "nats.hpp" +#include + +class ErrHandler { +public: + ErrHandler(const char* id) : id_(id) {} + + void handle(nats::Connection & nc, nats::Subscription & sub, natsStatus err) + { + std::cout << id_ << " error:" << nats::GetText(err) << std::endl; + } + +private: + const char* id_; +}; + +class Handler { +public: + Handler(const char* id) : id_(id) {} + + void msg(nats::Connection & nc, nats::Subscription & sub, nats::Msg && msg) + { + std::string data(msg.GetData(), msg.GetDataLength()); + std::cout << id_ << " received: " << msg.GetSubject() << " - " << data << std::endl; + } + +private: + const char* id_; +}; + +int main(int argc, char **argv) +{ + Handler hh("HandlerId"); + ErrHandler eh("MyErrorHandler"); + + nats::Open(10); + + nats::Options options; + options.SetErrorHandler(&eh); + + nats::Connection connection(options); + nats::Subscription sub = connection.Subscribe("subject", &hh); + + nats::Statistics stat; + + for (;;) + nats::Sleep(1000); + + nats::Close(); + + return 0; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 70b2102e3..e3a53e6cc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -50,6 +50,25 @@ if(NATS_BUILD_LIB_STATIC) target_compile_definitions(nats_static PRIVATE -DNATS_STATIC) endif(NATS_BUILD_LIB_STATIC) +# --------------------------- +# Create the bindings for C++ +# --------------------------- +if(NATS_BUILD_CPP_BINDINGS) + add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/nats.hpp + COMMAND ${Python3_EXECUTABLE} + bindings/cpp_generator.py + bindings/cpp_template.mako + nats.h + ${CMAKE_CURRENT_BINARY_DIR}/nats.hpp + DEPENDS + bindings/cpp_generator.py + bindings/cpp_template.mako + nats.h + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + add_custom_target(nats_cpp_bindings ALL DEPENDS ${CMAKE_BINARY_DIR}/nats.hpp) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/nats.hpp DESTINATION ${NATS_INCLUDE_DIR}/nats) +endif(NATS_BUILD_CPP_BINDINGS) + # -------------------------------------- # Install the libraries and header files # -------------------------------------- diff --git a/src/bindings/cpp_generator.py b/src/bindings/cpp_generator.py new file mode 100644 index 000000000..63eeb4401 --- /dev/null +++ b/src/bindings/cpp_generator.py @@ -0,0 +1,387 @@ +from clang.cindex import Index,CursorKind,TypeKind +from mako.template import Template +from sys import argv + +CONSTRUCTER_SUFFIX = '_Create' +DESTRUCTER_SUFFIX = '_Destroy' + +def get_type(type): + if type.spelling == '_Bool': + return 'bool' + return type.spelling + +class FunctionParameter: + def __init__(self, cursor): + type = cursor.type + element_count = 0 + if type.kind == TypeKind.CONSTANTARRAY: + type = type.element_type + element_count = cursor.type.element_count + self.type = get_type(type) + self.element_count = element_count + self.name = cursor.spelling + + @property + def plain_type(self): + return self.type.replace('*', '').strip() + + @property + def without_namespace(self, namespace): + return self.type[len(namespace):] + + @property + def argument(self): + return self.name + + @property + def parameter(self): + array = '' + if self.element_count: + array = '[%d]' % (self.element_count) + return '%s %s%s' % (self.type, self.name, array) + +class Function: + def __init__(self, cursor): + self.original_function = cursor.spelling + self.result_type = get_type(cursor.result_type) + self.brief_comment = cursor.brief_comment + self.raw_comment = cursor.raw_comment + + token = cursor.spelling.split('_', 2) + self.namespace = token[0][:4] + self.type = token[0][4:] + self.name = token[1] + + self.is_const = False + self.with_exception = False + + if self.result_type == 'natsStatus': + self.result_type = 'void' + self.with_exception = True + + self.parameters = [FunctionParameter(p) for p in cursor.get_arguments()] + + @property + def is_constructor(self): + return self.original_function.endswith(CONSTRUCTER_SUFFIX) + + @property + def is_destructor(self): + return self.original_function.endswith(DESTRUCTER_SUFFIX) + +class FunctionProxy: + def __init__(self, function): + self.function = function + + @property + def namespace(self): + return self.function.namespace + + @property + def name(self): + return self.function.name + + @property + def original_function(self): + return self.function.original_function + + @property + def with_exception(self): + return self.function.with_exception + + @property + def brief_comment(self): + return self.function.brief_comment + + @property + def raw_comment(self): + return self.function.raw_comment + + @property + def invocation(self): + ret = '%s(%s)' % (self.original_function, self.arguments) + if self.function.with_exception: + ret = 'Exception::CheckResult(%s)' % (ret) + return ret + + @property + def arguments(self): + return ', '.join(self.arguments_) + + @property + def parameters(self): + return ', '.join(self.parameters_) + + @property + def result_type(self): + return self.function.result_type + + @property + def suffix(self): + if self.is_const: + return ' const' + return '' + +class GlobalFunction(FunctionProxy): + def __init__(self, function): + FunctionProxy.__init__(self, function) + self.arguments_ = [p.argument for p in function.parameters] + self.parameters_ = [p.parameter for p in function.parameters] + +class Method(FunctionProxy): + def __init__(self, function, this, function_typedefs): + FunctionProxy.__init__(self, function) + self.arguments_ = [] + self.parameters_ = [] + self.forward_arguments_ = [] + self.forward_parameters_ = [] + + self.is_constructor = function.is_constructor + self.is_destructor = function.is_destructor + self.temporary_object = False + + type_number = 0 + self.template_parameter_ = [] + next_parameter_type = None + + for p in function.parameters: + argument = None + parameter = None + if p.type.replace('const ', '').startswith(this.original_type): + if p.type.count('*') == 2: + self.is_constructor = True + argument = '&self' + else: + argument = 'self' + self.is_const = p.type.startswith('const ') + else: + if p.type.count('*') == 2 and p.type.startswith(this.namespace): + self.temporary_object = p.plain_type.replace(this.namespace, '') + argument = '&ret.self' + else: + argument = p.argument + parameter = p.parameter + + self.arguments_.append(argument) + if parameter: + self.parameters_.append(parameter) + + if p.type in function_typedefs: + type_number = type_number + 1 + next_parameter_type = 'T%d' % (type_number) + + templateNamespace = p.type[:4] + template = p.type[4:] + + if templateNamespace != self.namespace: + template = '%s::%s' % (templateNamespace, template) + + args = (template, next_parameter_type, type_number) + self.forward_arguments_.append('&%sCallback<%s, callback%d>' % args) + self.template_parameter_ += [ + 'typename %s' % (next_parameter_type), + '%s<%s> callback%d' % args + ] + else: + if next_parameter_type: + self.forward_parameters_.append(parameter.replace('void', next_parameter_type)) + next_parameter_type = None + else: + self.forward_parameters_.append(parameter) + self.forward_arguments_.append(argument) + + @property + def result_type(self): + if self.temporary_object: + return self.temporary_object + return self.function.result_type + + @property + def forward_arguments(self): + return ', '.join(self.forward_arguments_) + + @property + def forward_parameters(self): + return ', '.join(self.forward_parameters_) + + @property + def template_parameter(self): + return ', '.join(self.template_parameter_) + + +class Class: + def __init__(self, namespace, typedef): + self.original_type = typedef.name + self.namespace = namespace.name + self.brief_comment = typedef.brief_comment + self.raw_comment = typedef.raw_comment + self.name = typedef.name[4:] + self.methods = [] + + def add_method(self, function, function_typedefs): + self.methods.append(Method(function, self, function_typedefs)) + +class Enum: + def __init__(self, cursor): + self.name = cursor.spelling + +class FunctionTypedef: + def __init__(self, cursor): + self.original_name = cursor.spelling + self.brief_comment = cursor.brief_comment + self.raw_comment = cursor.raw_comment + self.namespace = cursor.spelling[:4] + self.name = cursor.spelling[4:] + self.result_type = 'void' + self.original_parameters = [] + self.wrapper = [] + self.arguments_ = [] + self.parameters_ = [] + + for c in cursor.get_children(): + if c.kind == CursorKind.TYPE_REF: + self.result_type = c.type.spelling + else: + self.original_parameters.append(FunctionParameter(c)) + + self.has_closure = self.original_parameters[-1].name == 'closure' + if not self.has_closure: + return + + for p in self.original_parameters[:-1]: + argument = p.argument + parameter = p.parameter + if p.type.startswith(self.namespace) and p.type != 'natsStatus': + plain_type = p.plain_type + if plain_type == 'natsMsg': + argument = '%s(%s)' % (plain_type[4:], p.name) + parameter = plain_type[4:] + ' &&' + else: + argument = p.name + '_' + parameter = plain_type[4:] + ' &' + args = (plain_type[4:], argument, p.name) + self.wrapper.append('%s::WithoutDestuction %s(%s)' % args) + + self.arguments_.append(argument) + self.parameters_.append(parameter) + + @property + def arguments(self): + return ', '.join(self.arguments_) + + @property + def parameters(self): + return ', '.join(self.parameters_) + + @property + def callbackParameters(self): + return ', '.join([p.parameter for p in self.original_parameters]) + +class Namespace: + def __init__(self, name, include_guard = None): + self.name = name + self.classes = [] + self.functions = [] + self.function_typedefs = [] + self.include_guard = include_guard + + def add_class(self, name): + c = Class(self, name) + self.classes.append(c) + return c + + def add_function(self, value): + self.functions.append(GlobalFunction(value)) + + def add_function_typedef(self, value): + self.function_typedefs.append(value) + + def add_typedef(self, value): + pass + +class Typedef: + def __init__(self, cursor): + self.name = cursor.spelling + self.original_name = cursor.spelling + self.brief_comment = cursor.brief_comment + self.raw_comment = cursor.raw_comment + + +def parse(cursor): + functions = [] + enums = [] + typedefs = [] + function_typedefs = [] + + for cursor in cursor.get_children(): + if not cursor.spelling[:4] in ['nats', 'stan']: + continue + if cursor.kind == CursorKind.FUNCTION_DECL: + functions.append(Function(cursor)) + elif cursor.kind == CursorKind.TYPEDEF_DECL: + children = [c for c in cursor.get_children()] + if len(children) == 0: + typedefs.append(Typedef(cursor)) + continue + elif len(children) == 1: + if children[0].kind == CursorKind.ENUM_DECL: + enums.append(Enum(cursor)) + continue + if children[0].kind == CursorKind.TYPE_REF: + typedefs.append(Typedef(cursor)) + continue + function_typedefs.append(FunctionTypedef(cursor)) + + + namespaces = [ + Namespace('nats'), + Namespace('stan', 'defined(NATS_HAS_STREAMING)') + ] + function_names = [f.original_function for f in functions] + function_typedef_names = [ftd.original_name for ftd in filter(lambda ftd: ftd.has_closure, function_typedefs)] + classes = [] + + for ns in namespaces: + for ftd in filter(lambda ftd: ftd.original_name.startswith(ns.name), function_typedefs): + if ftd.has_closure: + ns.add_function_typedef(ftd) + + for td in filter(lambda td: td.name.startswith(ns.name), typedefs): + if td.original_name + '_Destroy' in function_names: + classes.append(ns.add_class(td)) + else: + ns.add_typedef(td) + + for f in filter(lambda f: f.original_function.startswith(ns.name), functions): + found_class = None + for c in classes: + if f.original_function.startswith(c.original_type): + found_class = c + break + if found_class: + found_class.add_method(f, function_typedef_names) + else: + ns.add_function(f) + + return namespaces + +def main(): + if len(argv) != 4: + print('usage: %s templatefile inputfile outputfile' % (argv[0])) + exit(-1) + + index = Index.create() + translation_unit = index.parse(None, [argv[2], '-DBUILD_IN_DOXYGEN', '-DNATS_HAS_STREAMING']) + if not translation_unit: + print('unable to load input') + exit(-1) + + namespaces = parse(translation_unit.cursor) + + template = Template(filename=argv[1]) + f = open(argv[3], 'w') + f.write(template.render(namespaces=namespaces)) + f.close() + +if __name__ == '__main__': + main() diff --git a/src/bindings/cpp_template.mako b/src/bindings/cpp_template.mako new file mode 100644 index 000000000..aa954d754 --- /dev/null +++ b/src/bindings/cpp_template.mako @@ -0,0 +1,219 @@ +// This file has been auto-generated. +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +// Copyright 2015-2020 The NATS Authors +// 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. + +#ifndef NATS_HPP_ +#define NATS_HPP_ + +#include "nats.h" +#include +% for ns in namespaces: + +% if ns.include_guard: +#if ${ns.include_guard} +% endif +namespace ${ns.name} { +% for c in ns.classes: +class ${c.name}; +% endfor +% for ftd in ns.function_typedefs: + +${ftd.raw_comment} +template +using ${ftd.name} = ${ftd.result_type} (T::*)(${ftd.parameters}); + +template callback> ${ftd.result_type} +${ftd.name}Callback(${ftd.callbackParameters}); +% endfor +% if ns.include_guard: +using nats::Exception; +% else: + +class Exception : public std::exception { + natsStatus status; + +public: + Exception(natsStatus s) : status(s) + { + } + + const natsStatus& + code() + { + return status; + } + + const char * + what() const noexcept + { + return natsStatus_GetText(status); + } + + static void + CheckResult(natsStatus status) + { + if (status != NATS_OK) + throw Exception(status); + } +}; +% endif +% for c in ns.classes: + +${c.raw_comment} +class ${c.name} { + class WithoutDestuction; +% for fc in ns.classes: +% if fc.name != c.name: + friend class ${fc.name}; +% endif +% endfor +% for ftd in ns.function_typedefs: + template callback> friend ${ftd.result_type} + ${ftd.name}Callback(${ftd.callbackParameters}); +% endfor + ${c.original_type} * self; + + void + disableDestroy() + { + self = nullptr; + } + +public: + explicit ${c.name}(${c.original_type}* ptr) : self(ptr) + { + } + + ${c.name}(${c.name}&& other) : self(other.Release()) + { + } + +% for m in c.methods: + /** \brief ${m.brief_comment} + * + * @see #${m.original_function}() + */ +% if m.is_constructor: + ${c.name}(${m.parameters}); +% elif m.is_destructor: + ~${c.name}(${m.parameters}); +% else: + ${m.result_type} + ${m.name}(${m.parameters})${m.suffix}; +% if m.template_parameter: + + template<${m.template_parameter}> ${m.result_type} + ${m.name}(${m.forward_parameters}); +%endif +% endif + +%endfor + operator const ${c.original_type} * () const + { + return self; + } + + operator ${c.original_type} * () + { + return self; + } + + [[nodiscard]] ${c.original_type} * + Release() + { + ${c.original_type} * ret = self; + self = nullptr; + return ret; + } +}; + +class ${c.name}::WithoutDestuction : public ${c.name} { +public: + WithoutDestuction(${c.original_type} * ptr) : ${c.name}(ptr) + { + } + + ~WithoutDestuction() + { + disableDestroy(); + } +}; +% endfor +% for f in ns.functions: + +/** \brief ${f.brief_comment} +* +* @see #${f.original_function}() +*/ +inline ${f.result_type} +${f.name}(${f.parameters}) +{ + return ${f.invocation}; +} +% endfor + +% for c in ns.classes: +% for m in c.methods: +% if m.is_constructor: +inline ${c.name}::${c.name}(${m.parameters}) +{ + ${m.invocation}; +% elif m.is_destructor: +inline ${c.name}::~${c.name}(${m.parameters}) +{ + ${m.invocation}; +% else: +inline ${m.result_type} +${c.name}::${m.name}(${m.parameters})${m.suffix} +{ +% if m.temporary_object: + ${m.temporary_object} ret(nullptr); + ${m.invocation}; + return ret; +% else: + return ${m.invocation}; +% endif +% endif +} +% if m.template_parameter: + +template<${m.template_parameter}> inline ${m.result_type} +${c.name}::${m.name}(${m.forward_parameters}) +{ + return ${m.name}(${m.forward_arguments}); +} +% endif + +%endfor +%endfor +% for ftd in ns.function_typedefs: +template callback> ${ftd.result_type} +${ftd.name}Callback(${ftd.callbackParameters}) +{ +% for w in ftd.wrapper: + ${w}; +% endfor + T * self = static_cast(closure); + return (self->*callback)(${ftd.arguments}); +} + +% endfor +} // namespace ${ns.name} +% if ns.include_guard: +#endif +% endif +% endfor + +#endif /* NATS_HPP_ */