/* Copyright (C) 2006 intra2net.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /*! \mainpage libt2n - (talk to neighbor) \section intro_sec Introduction libt2n (talk to neighbor) is a C++ library for inter-process communication (IPC, s.a. http://en.wikipedia.org/wiki/Inter-process_communication) with an additional code generator to make remote procedure calls simple. \par XXX: improve this paragraph: The input for the code generator is standard C++ code (in fact we use gccxml to parse the C++ code and the code generator takes the XML as input) and you mark the procedures you want to expose to other processes. It then generates the stubs needed. The exported procedures can be grouped. For each group the code generator is called and generates 6 output files: group_common.hxx, group_common.cpp, group_client.hxx, group_client.cpp, group_server.hxx, group_server.cpp. The _common files are used by client and server whereas the _client files are used by the client and the _server files by the server only. \par To simplify the build process a Makefile snippet is provided that allows to create a server program and a client library (including a corresponding .pc file) using the autotools easily. \section install_sec Installation \subsection requirements Requirements - boost (serialization ) - gccxml - libxmlpp \subsection recommended Recommended - pkg-config - autotools (automake, autoconf, libtool) \subsection Compilation \verbatim ./configure && make install \endverbatim \section usage Usage example In this example we create two packages using the autotools: - server program and client library to connect to the server. The server exports a simple procedure using one group: "t2nexample" - client program using the library \subsection server Example server program and client library \par The procedure to export (input for the code generator - libt2n-codegen): t2nexample.cpp: \verbinclude libt2n-example1/t2nexample.cpp \par Required includes go into the group header file: t2nexample.hxx: libt2n uses boost for serialization. This means all types involved in a remote procedure call must be boost serializable. In this example we use std::string and boost already provides serialization for std::string in the boost/serialization/string.hpp header file. \verbinclude libt2n-example1/t2nexample.hxx \par The server program: \verbinclude libt2n-example1/server.cpp \par Using autoconf and automake to build a example server program and a client library. In the configure.in(.ac) we put a check for libt2n: \verbinclude libt2n-example1/configure.in Writing the Makefile.am isn't difficult either: \verbinclude libt2n-example1/Makefile.am \par Build and install the package \verbatim autoreconf -f -i && ./configure && make install \endverbatim \subsection client Client using the library Using the library is as simple as using any other library using pkg-config (the pkg-config .pc file is created automatically by the included Makefile snippet) \par We only have to check that the library is installed \verbinclude libt2n-example1-client/configure.in \par The Makefile.am needs nothing special \verbinclude libt2n-example1-client/Makefile.am \par The client program \verbinclude libt2n-example1-client/client.cpp \par Build and install the package \verbatim autoreconf -f -i && ./configure && make install \endverbatim \par Test \verbatim $ cd /tmp $ file socket socket: cannot open `socket' (No such file or directory) $ libt2n-example1-server & [1] 7711 $ file socket socket: socket $ libt2n-example1-client && echo ok ok $ kill %1 $ rm socket \endverbatim */ /*! \example t2nexample.cpp example input for libt2n-codegen */ #include #include #include #include #include #include #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif //! map group to class name std::string groupClass(const std::string &group) { return std::string("cmd_group_")+group; } //! convert string to upper case std::string toupper(std::string s) { for (unsigned i=0; is.length() ) || ( std::string(s,0,prefix.length())!=prefix ) ) return error; return std::string(s, prefix.length(), s.length()-prefix.length()); } //! get child element by id /*! \return pointer to element having id or null on error \todo find libxmlpp pendant */ const xmlpp::Element* get_element_by_id(const xmlpp::Element* element, const std::string &id) { const xmlpp::Attribute* cid = element->get_attribute("id"); if ( cid && ( cid->get_value() == id)) return element; //Recurse through child nodes: xmlpp::Node::NodeList list = element->get_children(); for(xmlpp::Node::NodeList::iterator iter = list.begin(); iter != list.end(); ++iter) { const xmlpp::Element* element = dynamic_cast(*iter); if (element) { const xmlpp::Element* match = get_element_by_id(element, id); if (match) return match; } } return NULL; } std::string get_file(const xmlpp::Element* root, const std::string &file_id) { std::string error; const xmlpp::Element* e=get_element_by_id(root, file_id); if ((!e)||(!e->get_attribute("name"))) return error; return e->get_attribute("name")->get_value(); } //! get namespace by id /*! \return namespace name or empty string on error */ std::string get_namespace(const xmlpp::Element* root, const std::string &id) { std::string error; const xmlpp::Element* element(get_element_by_id(root, id)); if ((!element)||(!element->get_attribute("name"))) return error; return element->get_attribute("name")->get_value(); } //! extract group from attributes std::string extract_group(const std::string &attrs) { // todo: improve this std::string error; std::string to_match("gccxml(libt2n-"); std::string::size_type p(attrs.find(to_match)); if (p==std::string::npos) return error; std::string group(attrs, p+to_match.length(), attrs.length()); p=group.find_first_of(')'); assert(p!=std::string::npos); return std::string(group,0,p); } struct type_info { std::string name; std::string noref_name; bool operator==(const type_info& o) {return (name==o.name) && (noref_name == o.noref_name);} std::string noref() const {return noref_name.empty() ? name : noref_name;} }; std::ostream &operator<<(std::ostream &o, const type_info &t) { o << t.name; return o; } struct parse_error : public std::runtime_error { parse_error(const std::string &file, unsigned line, const std::string &msg) : std::runtime_error(file+":"+boost::lexical_cast(line)+": error: "+msg) {} }; //! get type by id /*! \return type name or empty string on error */ type_info get_type(const xmlpp::Element* root, const std::string &id) { type_info error; const xmlpp::Element* element(get_element_by_id(root, id)); if (!element) return error; // TODO: not yet complete // if we recurse - when do we stop? // if it is a typedef? yes? (hmm if the typedef is in the file parsed this will not work) // TODO: const and reference types handling is a ugly hack std::string tag(element->get_name()); if (tag=="ReferenceType") { assert(element->get_attribute("type")); type_info ret(get_type(root, element->get_attribute("type")->get_value())); if (ret==error) return error; // at the moment we only support const & // todo: nice error message! if ((ret.noref_name=strip(ret.name,"const ")).empty()) return error; ret.name=ret.name+"&"; return ret; }else if (tag=="CvQualifiedType") { assert(element->get_attribute("type")); type_info ret(get_type(root, element->get_attribute("type")->get_value())); if (ret==error) return error; ret.name=std::string("const ")+ret.name; return ret; }else if (tag=="PointerType") { // not yet supported return error; } assert(element->get_attribute("name")); type_info ret; if (element->get_attribute("context")) { ret.name=get_namespace(root, element->get_attribute("context")->get_value()); if (ret.name!="::") ret.name+="::"; else // do not explicitely add :: ret.name=""; } ret.name+=element->get_attribute("name")->get_value(); return ret; } struct t2n_procedure { typedef std::list > Args; type_info ret_type; std::string name; std::string mangled; Args args; std::string ret_classname() const { return name+mangled+"_res"; } std::string cmd_classname() const { return name+mangled+"_cmd"; } }; std::ostream &operator<<(std::ostream &o, const t2n_procedure::Args &args) { for (t2n_procedure::Args::const_iterator it=args.begin();it!=args.end();++it) { if (it!=args.begin()) o << ", "; o << it->second << " " << it->first; } return o; } std::ostream &operator<<(std::ostream &o, const t2n_procedure &f) { o << f.ret_type << " " << f.name << "(" << f.args << ")"; return o; } class Parser { public: Parser(const std::string &fname) : m_fname(fname) {} std::list get_procedures() { xmlpp::DomParser parser; // parser.set_validate(); parser.set_substitute_entities(); //We just want the text to be resolved/unescaped automatically. parser.parse_file(m_fname); if(parser) { //Walk the tree: const xmlpp::Node* pNode = parser.get_document()->get_root_node(); //deleted by DomParser. const xmlpp::Element* root = dynamic_cast(pNode); assert(root); visit_node(root); } return m_procedures; } protected: std::string m_fname; std::list m_procedures; void parse_function(const xmlpp::Element* root, const xmlpp::Node* node) { const xmlpp::Element* element = dynamic_cast(node); if (!element) return; const xmlpp::Attribute* attributes = element->get_attribute("attributes"); const xmlpp::Attribute* name = element->get_attribute("name"); const xmlpp::Attribute* mangled = element->get_attribute("mangled"); const xmlpp::Attribute* returns = element->get_attribute("returns"); if ((!attributes)||(!name)||(!mangled)||(!returns)) return; // check wether the procedure is marked (TODO: improve) // attributes are speparated by spaces? t2n_procedure f; if (extract_group(attributes->get_value()).empty()) return; // we need the return type f.ret_type=get_type(root, returns->get_value()); f.name=name->get_value(); f.mangled=mangled->get_value(); xmlpp::Node::NodeList list = node->get_children("Argument"); for(xmlpp::Node::NodeList::iterator iter = list.begin(); iter != list.end(); ++iter) { const xmlpp::Element* arg = dynamic_cast(*iter); if ( arg ) { assert(arg->get_name() == "Argument"); assert(arg->get_attribute("name")); assert(arg->get_attribute("type")); f.args.push_back(std::pair(arg->get_attribute("name")->get_value(), get_type(root, arg->get_attribute("type")->get_value()))); // todo: ugly - could be any other error if (f.args.back().second.name.empty()) { assert(element->get_attribute("file")); assert(element->get_attribute("line")); throw parse_error(get_file(root, element->get_attribute("file")->get_value()), boost::lexical_cast(element->get_attribute("line")->get_value())-1, std::string("type of parameter `")+f.args.back().first+"' not (yet?) supported"); } } } std::cerr << "Found function: " << f << std::endl; m_procedures.push_back(f); } void visit_node(const xmlpp::Element* root, const xmlpp::Node* node = NULL, unsigned int indentation = 0) { if (!node) node=root; const xmlpp::ContentNode* nodeContent = dynamic_cast(node); const xmlpp::TextNode* nodeText = dynamic_cast(node); const xmlpp::CommentNode* nodeComment = dynamic_cast(node); if(nodeText && nodeText->is_white_space()) //Let's ignore the indenting - you don't always want to do this. return; std::string nodename = node->get_name(); if(!nodeText && !nodeComment && !nodename.empty()) //Let's not say "name: text". { if (node->get_name() == "Function") parse_function(root, node); } if(!nodeContent) { //Recurse through child nodes: xmlpp::Node::NodeList list = node->get_children(); for(xmlpp::Node::NodeList::iterator iter = list.begin(); iter != list.end(); ++iter) { visit_node(root, *iter, indentation + 2); //recursive } } } }; void output_common_hpp(std::ostream &o, const std::string &group, const std::list &procs) { o << "class " << groupClass(group) << " : public libt2n::command\n" << "{\n" << "private:\n" << " friend class boost::serialization::access;\n" << " template\n" << " void serialize(Archive & ar, const unsigned int /* version */)\n" << " {ar & BOOST_SERIALIZATION_BASE_OBJECT_NVP(libt2n::command);}\n" << "};\n"; for (std::list::const_iterator it=procs.begin();it!=procs.end();++it) { o << "class " << it->ret_classname() << " : public libt2n::result\n" << "{\n" << "private:\n" << " " << it->ret_type << " res;\n" << " friend class boost::serialization::access;\n" << " template\n" << " void serialize(Archive & ar, const unsigned int /* version */)\n" << " {\n" << " ar & BOOST_SERIALIZATION_BASE_OBJECT_NVP(libt2n::result);\n" << " ar & BOOST_SERIALIZATION_NVP(res);\n" << " }\n" << "public:\n" << " " << it->ret_classname() << "() {}\n" << " " << it->ret_classname() << "(const " << it->ret_type << " &_res) : res(_res) {}\n" << " " << it->ret_type << " get_data() { return res; }\n" << "};\n"; } for (std::list::const_iterator it=procs.begin();it!=procs.end();++it) { o << "class " << it->cmd_classname() << " : public " << groupClass(group) << "\n" << "{\n" << "private:\n"; for (t2n_procedure::Args::const_iterator ait=it->args.begin();ait!=it->args.end();++ait) { o << " " << ait->second.noref() << " " << ait->first << ";\n"; } o << " friend class boost::serialization::access;\n" << " template\n" << " void serialize(Archive & ar, const unsigned int /* version */)\n" << " {\n" << " ar & BOOST_SERIALIZATION_BASE_OBJECT_NVP(" << groupClass(group) << ");\n"; for (t2n_procedure::Args::const_iterator ait=it->args.begin();ait!=it->args.end();++ait) { o << " ar & BOOST_SERIALIZATION_NVP(" << ait->first << ");\n"; } // default constructor o << " }\n" << "\n" << "public:\n" << " " << it->cmd_classname() << "() {}\n"; // constructor taking all arguments o << " " << it->cmd_classname() << "("; for (t2n_procedure::Args::const_iterator ait=it->args.begin();ait!=it->args.end();++ait) { if (ait!=it->args.begin()) o << ", "; o << ait->second << " _" << ait->first; } o << ") : "; for (t2n_procedure::Args::const_iterator ait=it->args.begin();ait!=it->args.end();++ait) { if (ait!=it->args.begin()) o << ", "; o << ait->first << "(_" << ait->first << ")"; } o << " {}\n" << " libt2n::result* operator()();\n" << "};\n"; } } void output_common_cpp(std::ostream &o, const std::string &group, const std::list &procs, const std::string &common_hpp) { o << "#include \"" << common_hpp << "\"\n" << "#include \n" << "\n" << "/* register types with boost serialization */\n"; o << "BOOST_CLASS_EXPORT(" << groupClass(group) << ")\n"; for (std::list::const_iterator it=procs.begin();it!=procs.end();++it) { o << "BOOST_CLASS_EXPORT("<ret_classname()<<")\n" << "BOOST_CLASS_EXPORT("<cmd_classname()<<")\n"; } } void output_client_hpp(std::ostream &o, const std::string &group, const std::list &procs) { o << "#include \n"; o << "class " << groupClass(group) << "_client : public libt2n::command_client\n" << "{\n" << "public:\n" << groupClass(group) << "_client(libt2n::client_connection &_c,\n" << " long long _command_timeout_usec=command_timeout_usec_default,\n" << " long long _hello_timeout_usec=hello_timeout_usec_default)\n" << " : libt2n::command_client(_c,_command_timeout_usec,_hello_timeout_usec)\n" << " {}\n"; for (std::list::const_iterator pit=procs.begin();pit!=procs.end();++pit) { o << " " << *pit << ";\n"; } o << "};\n"; } void output_client_cpp(std::ostream &o, const std::string &group, const std::list &procs, const std::string &common_hpp, const std::string &common_cpp, const std::string &client_hpp) { o << "#include \"" << client_hpp << "\"\n" << "#include \"" << common_hpp << "\"\n" << "// fake\n"; for (std::list::const_iterator it=procs.begin();it!=procs.end();++it) { o << "libt2n::result* " << it->cmd_classname() << "::operator()() { return NULL; }\n"; } for (std::list::const_iterator pit=procs.begin();pit!=procs.end();++pit) { o << pit->ret_type << " " << groupClass(group) << "_client::" << pit->name << "(" << pit->args << ")\n" << "{\n" << " libt2n::result_container rc;\n" << " send_command(new " << pit->cmd_classname() << "("; for (t2n_procedure::Args::const_iterator ait=pit->args.begin();ait!=pit->args.end();++ait) { if (ait!=pit->args.begin()) o << ", "; o << ait->first; } o << "), rc);\n" << " " << pit->ret_classname() << "* res=dynamic_cast<" << pit->ret_classname() << "*>(rc.get_result());\n" << " if (!res) throw libt2n::t2n_communication_error(\"result object of wrong type\");\n" << " return res->get_data();\n" << "}\n"; } // include in this compilation unit to ensure the compilation unit is used // see also: // http://www.google.de/search?q=g%2B%2B+static+initializer+in+static+library o << "#include \"" << common_cpp << "\"\n"; } void output_server_hpp(std::ostream &o, const std::string & /* group */, const std::list &procs, const std::string &common_hpp) { o << "#include \"" << common_hpp << "\"\n"; // output function declarations for (std::list::const_iterator it=procs.begin();it!=procs.end();++it) o << *it << ";\n"; } void output_server_cpp(std::ostream &o, const std::string &group, const std::list &procs, const std::string &common_hpp, const std::string &common_cpp) { o << "#include \"" << common_hpp << "\"\n"; for (std::list::const_iterator it=procs.begin();it!=procs.end();++it) { o << *it << ";\n"; o << "libt2n::result* " << it->cmd_classname() << "::operator()() { return new " << it->ret_classname() << "(" << it->name << "("; for (t2n_procedure::Args::const_iterator ait=it->args.begin();ait!=it->args.end();++ait) { if (ait!=it->args.begin()) o << ", "; o << ait->first; } o << ")); }\n"; } o << "#include \"" << common_cpp << "\"\n"; } struct header_file : public std::ofstream { header_file(const char* fname) : std::ofstream(fname) { std::cerr << "create header: '" << fname << "'" << std::endl; std::string macro(replace(toupper(fname),'.','_')); *this << "// automatically generated code (generated by libt2n-codegen " << VERSION << ") - do not edit\n" << std::endl; *this << "#ifndef " << macro << "\n" << "#define " << macro << "\n"; } ~header_file() { *this << "#endif" << std::endl; } }; struct cpp_file : public std::ofstream { cpp_file(const char* fname) : std::ofstream(fname) { std::cerr << "create cpp: '" << fname << "'" << std::endl; *this << "// automatically generated code - do not edit\n" << std::endl; } }; int main(int argc, char* argv[]) { // todo: maybe use getopt if ((argc>1)&&(std::string(argv[1])=="--version")) { std::cerr << VERSION << std::endl; return 0; } if (argc < 3) { std::cerr << "Usage: " << argv[0] << "default-group gccxml-file1 gccxml-file2 ... " << std::endl; return 1; } try{ std::string group(argv[1]); std::list xmlfiles; for (int i=2;i procedures; for (std::list::iterator it=xmlfiles.begin();it!=xmlfiles.end();++it) { std::cerr << "Parse " << *it << std::endl; Parser parser(*it); const std::list &p(parser.get_procedures()); std::copy(p.begin(), p.end(), std::back_inserter(procedures)); } std::cerr << "Procedures:" << std::endl; for (std::list::const_iterator it=procedures.begin();it!=procedures.end();++it) std::cerr << *it << ";" << std::endl; std::string common_hpp_fname(prefix+"common.hxx"); std::string common_cpp_fname(prefix+"common.cpp"); std::string client_hpp_fname(prefix+"client.hxx"); std::string client_cpp_fname(prefix+"client.cpp"); std::string server_hpp_fname(prefix+"server.hxx"); std::string server_cpp_fname(prefix+"server.cpp"); header_file common_hpp(common_hpp_fname.c_str()); common_hpp << "// boost serialization is picky about order of include files => we have to include this one first\n" << "#include \"codegen-stubhead.hxx\"\n" << "#include \"" << group << ".hxx\"\n"; output_common_hpp(common_hpp, group, procedures); cpp_file common_cpp(common_cpp_fname.c_str()); output_common_cpp(common_cpp, group, procedures, common_hpp_fname); header_file client_hpp(client_hpp_fname.c_str()); client_hpp << "// boost serialization is picky about order of include files => we have to include this one first\n" << "#include \"codegen-stubhead.hxx\"\n" << "#include \"" << group << ".hxx\"\n"; output_client_hpp(client_hpp, group, procedures); cpp_file client_cpp(client_cpp_fname.c_str()); output_client_cpp(client_cpp, group, procedures, common_hpp_fname, common_cpp_fname, client_hpp_fname); header_file server_hpp(server_hpp_fname.c_str()); output_server_hpp(server_hpp, group, procedures, common_hpp_fname); cpp_file server_cpp(server_cpp_fname.c_str()); output_server_cpp(server_cpp, group, procedures, common_hpp_fname, common_cpp_fname); }catch(const parse_error &e){ std::cerr << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; }