diff options
author | Alan Conway <aconway@apache.org> | 2007-07-31 20:00:10 +0000 |
---|---|---|
committer | Alan Conway <aconway@apache.org> | 2007-07-31 20:00:10 +0000 |
commit | c11f9a79eec63da7aa6e6dac248a689a9d461beb (patch) | |
tree | 7c89f9804904dd3b99f3b3e564d14278ee3a7044 /cpp | |
parent | 82d128b33fac55b8618d593c89283356ee4e192b (diff) | |
download | qpid-python-c11f9a79eec63da7aa6e6dac248a689a9d461beb.tar.gz |
Ruby code generator for C++.
Not yet in active use yet but two sample templates are provided.
Note: same dependency story as java codegen: distribution has pre-generated
code so ruby not required to build a distro. Only required for svn working
copy builds.
git-svn-id: https://svn.apache.org/repos/asf/incubator/qpid/trunk/qpid@561468 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'cpp')
-rw-r--r-- | cpp/rubygen/README | 20 | ||||
-rwxr-xr-x | cpp/rubygen/amqpgen.rb | 208 | ||||
-rwxr-xr-x | cpp/rubygen/cppgen.rb | 126 | ||||
-rwxr-xr-x | cpp/rubygen/samples/Operations.rb | 85 | ||||
-rwxr-xr-x | cpp/rubygen/samples/Proxy.rb | 153 | ||||
-rwxr-xr-x | cpp/rubygen/samples/runme | 18 |
6 files changed, 610 insertions, 0 deletions
diff --git a/cpp/rubygen/README b/cpp/rubygen/README new file mode 100644 index 0000000000..43111d4c4f --- /dev/null +++ b/cpp/rubygen/README @@ -0,0 +1,20 @@ +RUBY CODE GENERATOR + +amqpgen.rb: builds an AMQP model from XML files and provides generic code generation functions (e.g. capitalization of names etc) + +cppgen.rb: C++ specific code generation functions. + +A template is a ruby file that generates one or more source files. + +For an example run + samples/runme test <amqp.xml files> + +The first argument is a directory for generated files, remaining arguments +are xml files. + + + + + + + diff --git a/cpp/rubygen/amqpgen.rb b/cpp/rubygen/amqpgen.rb new file mode 100755 index 0000000000..3fc66525d8 --- /dev/null +++ b/cpp/rubygen/amqpgen.rb @@ -0,0 +1,208 @@ +# +# Generic AMQP code generation library. +# + +require 'delegate' +require 'rexml/document' +require 'pathname' +include REXML + +# Handy String functions for converting names. +class String + # Convert to CapitalizedForm. + def caps() gsub( /(^|\W)(\w)/ ) { |m| $2.upcase } end + + # Convert to underbar_separated_form. + def bars() tr('- .','_'); end + + # Convert to lowerCaseCapitalizedForm + def lcaps() gsub( /\W(\w)/ ) { |m| $1.upcase } end +end + +# Sort an array by name. +class Array + def sort_by_name() + sort() { |a,b| a.name <=> b.name } + end +end + +# Add collect to Elements +class Elements + def collect(xpath, &block) + result=[] + each(xpath) { |el| result << yield(el) } + result + end +end + +# An AmqpElement extends (delegates to) a REXML::Element +# +# NB: AmqpElements cache various values, they assume that +# the XML model does not change after the AmqpElement has +# been created. +# +class AmqpElement < DelegateClass(Element) + def initialize(xml, amqp) super(xml); @amqp_parent=amqp; end + + attr_reader :amqp_parent + + # Return the name attribute, not the element name. + def name() attributes["name"]; end + + def amqp_root() + amqp_parent ? amqp_parent.amqp_root : self; + end +end + +# AMQP field element +class AmqpField < AmqpElement + def initialize(xml, amqp) super; end; + + # Get AMQP type for a domain name. + def domain_type(name) + domain=elements["/amqp/domain[@name='#{name}']"] + (domain and domain.attributes["type"] or name) + end + + # Get the AMQP type of this field. + def field_type() + d=attributes["domain"] + dt=domain_type d if d + (dt or attributes["type"]) + end +end + +# AMQP method element +class AmqpMethod < AmqpElement + def initialize(xml, amqp) super; end + + def fields() + @cache_fields ||= elements.collect("field") { |f| AmqpField.new(f,self); } + end + + # Responses to this method (0-9) + def responses() + @cache_responses ||= elements.collect("response") { |el| new AmqpMethod(el,self) } + end + + # Methods this method responds to (0-9) + def responds_to() + @cache_responds_to ||= elements.collect("../method/response[@name='#{attributes['name']}']") { |el| + AmqpMethod.new(el.parent, amqp_parent) + } + end + + def request?() responds_to().empty?; end + def response?() not request?; end +end + +# AMQP class element. +class AmqpClass < AmqpElement + def initialize(xml,amqp) super; end + + def methods() + @cache_methods ||= elements.collect("method") { |el| + AmqpMethod.new(el,self) + }.sort_by_name + end + + # chassis should be "client" or "server" + def methods_on(chassis) + elements.collect("method/chassis[@name='#{chassis}']/..") { |m| + AmqpMethod.new(m,self) + }.sort_by_name + end +end + +# AMQP root element. +class AmqpRoot < AmqpElement + + # Initialize with output directory and spec files from ARGV. + def initialize(*specs) + specs.size or raise "No XML spec files." + specs.each { |f| File.exists?(f) or raise "Invalid XML file: #{f}"} + super Document.new(File.new(specs.shift)).root, nil + specs.each { |s| # Merge in additional specs + root=Document.new(File.new(s)).root + merge(self,root) + } + end + + def version() + attributes["major"]+"-"+attributes["minor"] + end + + def classes() + @cache_classes ||= elements.collect("class") { |c| AmqpClass.new(c,self) }.sort_by_name + end + + # Return all methods on chassis for all classes. + def methods_on(chassis) + classes.collect { |c| + c.methods_on(chassis) + }.flatten + end + + # Merge contents of elements. + def merge(to,from) + from.elements.each { |from_child| + tag,name = from_child.name, from_child.attributes["name"] + to_child=to.elements["./#{tag}[@name='#{name}']"] + to_child ? merge(to_child, from_child) : to.add(from_child.clone) } + end + + private :merge + +end + +# Base class for code generators. +# Supports setting a per-line prefix, useful for e.g. indenting code. +# +class Generator + # Takes directory for output or "-", meaning print file names that + # would be generated. + def initialize (outdir, amqp) + @amqp=amqp + @outdir=outdir + @prefix='' # For indentation or comments. + @indentstr=' ' # One indent level. + raise "Invalid output directory: #{outdir}" unless @outdir=="-" or File.directory?(@outdir) + end + + # Create a new file, set @out. + def file(file) + puts file + if (@outdir != "-") + path=Pathname.new "#{@outdir}/#{file}" + path.parent.mkpath + path.open('w') { |@out| yield } + end + end + + # Append multi-line string to generated code, prefixing each line. + def gen (str) + str.each_line { |line| + @out << @prefix unless @midline + @out << line + @midline = nil + } + # Note if we stopped mid-line + @midline = /[^\n]\z/ === str + end + + # Generate code with added prefix. + def prefix(add) + save=@prefix + @prefix+=add + yield + @prefix=save + end + + # Generate indented code + def indent(n=1,&block) prefix(@indentstr * n,&block); end + + attr_accessor :out +end + + + diff --git a/cpp/rubygen/cppgen.rb b/cpp/rubygen/cppgen.rb new file mode 100755 index 0000000000..3e3800c4cd --- /dev/null +++ b/cpp/rubygen/cppgen.rb @@ -0,0 +1,126 @@ +#!/usr/bin/ruby +# +# General purpose C++ code generation. +# +require 'amqpgen' +require 'set' + +Copyright=<<EOS +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + */ + +EOS + +CppKeywords = Set.new(["and", "and_eq", "asm", "auto", "bitand", + "bitor", "bool", "break", "case", "catch", "char", + "class", "compl", "const", "const_cast", "continue", + "default", "delete", "do", "DomainInfo", "double", + "dynamic_cast", "else", "enum", "explicit", "extern", + "false", "float", "for", "friend", "goto", "if", + "inline", "int", "long", "mutable", "namespace", "new", + "not", "not_eq", "operator", "or", "or_eq", "private", + "protected", "public", "register", "reinterpret_cast", + "return", "short", "signed", "sizeof", "static", + "static_cast", "struct", "switch", "template", "this", + "throw", "true", "try", "typedef", "typeid", + "typename", "union", "unsigned", "using", "virtual", + "void", "volatile", "wchar_t", "while", "xor", + "xor_eq"]) +# Names that need a trailing "_" to avoid clashes. +CppMangle = CppKeywords+Set.new(["string"]) + +class String + def cppsafe() + CppMangle.include?(self) ? self+"_" : self + end +end + +# Additional methods for AmqpField. +class AmqpField + def cppname() @cache_cppname ||= name.lcaps.cppsafe; end + def cpptype() @cache_cpptype ||= amqp_root.param_type(field_type); end + def type_name () @type_name ||= cpptype+" "+cppname; end +end + +# Additional methods for AmqpMethod +class AmqpMethod + def cppname() @cache_cppname ||= name.lcaps.cppsafe; end + def param_names() @cache_param_names ||= fields.collect { |f| f.cppname }; end + def signature() @cache_signature ||= fields.collect { |f| f.cpptype+" "+f.cppname }; end + def body_name() @cache_body_name ||= amqp_parent.name.caps+name.caps+"Body"; end +end + +# Additional methos for AmqpRoot +class AmqpRoot + # FIXME aconway 2007-06-20: fix u_int types, should be uint + CppTypeMap={ + "bit"=> ["bool"], + "octet"=>["u_int8_t"], + "short"=>["u_int16_t"], + "long"=>["u_int32_t"], + "longlong"=>["u_int64_t"], + "timestamp"=>["u_int64_t"], + "longstr"=>["string", "const string&"], + "shortstr"=>["string", "const string&"], + "table"=>["FieldTable", "const FieldTable&", "const FieldTable&"], + "content"=>["Content", "const Content&", "const Content&"] + } + + def lookup(amqptype) + CppTypeMap[amqptype] or raise "No cpp type for #{amqptype}"; + end + + def member_type(amqptype) lookup(amqptype)[0]; end + def param_type(amqptype) t=lookup(amqptype); t[1] or t[0]; end + def return_type(amqptype) t=lookup(amqptype); t[2] or t[0]; end +end + +# Additional methods for AmqpClass +class AmqpClass + def cppname() @cache_cppname ||= name.caps; end +end + +class CppGen < Generator + def initialize(outdir, *specs) + super(outdir,*specs) + end + + # Write a header file. + def h_file(path) + guard=path.upcase.tr('./-','_') + file(path) { + gen "#ifndef #{guard}\n" + gen "#define #{guard}\n" + gen Copyright + yield + gen "#endif /*!#{guard}*/\n" + } + end + + # Write a .cpp file. + def cpp_file(path) + file (path) do + gen Copyright + yield + end + end +end + diff --git a/cpp/rubygen/samples/Operations.rb b/cpp/rubygen/samples/Operations.rb new file mode 100755 index 0000000000..1c245ca188 --- /dev/null +++ b/cpp/rubygen/samples/Operations.rb @@ -0,0 +1,85 @@ +#!/usr/bin/env ruby +# Usage: output_directory xml_spec_file [xml_spec_file...] +# +$: << '..' +require 'cppgen' + +class OperationsGen < CppGen + + def initialize(chassis, outdir, amqp) + super(outdir, amqp) + @chassis=chassis + @classname="AMQP_#{@chassis.caps}Operations" + end + + def handler_method (m) + gen "\nvirtual void #{m.cppname}(" + gen m.signature.join(",\n") + gen ") = 0;\n" + end + + def handler_classname(c) c.name.caps+"Handler"; end + + def handler_class(c) + handlerclass=handler_classname c + gen <<EOS +// ==================== class #{handlerclass} ==================== +class #{handlerclass} : Invocable { + // Constructors and destructors + public: + #{handlerclass}(){}; + virtual ~#{handlerclass}() {} + // Protocol methods +EOS + c.methods_on(@chassis).each { |m| handler_method(m) } + gen <<EOS +}; // class #{handlerclass} + + +EOS + end + + def handler_get(c) + handlerclass=handler_classname c + gen "virtual #{handlerclass}* get#{handlerclass}() = 0;\n" + end + + def generate() + h_file("#{@classname}.h") { + gen <<EOS +#include <sstream> +#include "qpid/framing/ProtocolVersion.h" + +namespace qpid { +namespace framing { + +class #{@classname} { + + public: + virtual ~#{@classname}() {} + + virtual ProtocolVersion getVersion() const = 0; + + // Include framing constant declarations + #include "AMQP_Constants.h" + + // Inner classes +EOS + indent { @amqp.classes.each { |c| handler_class(c) } } + gen <<EOS + + // Method handler get methods + +EOS + indent { @amqp.classes.each { |c| handler_get(c) } } + gen <<EOS +}; /* class #{@classname} */ +} +EOS +} + end +end + +OperationsGen.new("client",ARGV[0], Amqp).generate() +OperationsGen.new("server",ARGV[0], Amqp).generate() + diff --git a/cpp/rubygen/samples/Proxy.rb b/cpp/rubygen/samples/Proxy.rb new file mode 100755 index 0000000000..f7765f3729 --- /dev/null +++ b/cpp/rubygen/samples/Proxy.rb @@ -0,0 +1,153 @@ +#!/usr/bin/env ruby +$: << ".." # Include .. in load path +require 'cppgen' + +class ProxyGen < CppGen + + def initialize(chassis, outdir, amqp) + super(outdir, amqp) + @chassis=chassis + @classname="AMQP_#{@chassis.caps}Proxy" + end + + def include(m) gen "#include \"#{m.body_name}.h\"\n"; end + + def proxy_member(c) c.name.lcaps+"Proxy"; end + + def inner_class_decl(c) + cname=c.name.caps + gen <<EOS + // ==================== class #{cname} ==================== + class #{cname} + { + private: + ChannelAdapter& channel; + + public: + // Constructors and destructors + + #{cname}(ChannelAdapter& ch) : + channel(ch) {} + virtual ~#{cname}() {} + + static #{cname}& get(#{@classname}& proxy) { return proxy.get#{cname}();} + + // Protocol methods +EOS + indent(2) { c.methods_on(@chassis).each { |m| inner_method_decl(m) } } + gen "\n }; // class #{cname}\n\n" + end + + def inner_method_decl(m) + gen "virtual void #{m.cppname}(#{m.signature.join(",\n ")})\n\n"; + end + + def inner_class_defn(c) + cname=c.cppname + gen "// ==================== class #{cname} ====================\n" + c.methods_on(@chassis).each { |m| inner_method_defn(m, cname) } + end + + def inner_method_defn(m,cname) + if m.response? + rettype="void" + ret="" + sigadd=["RequestId responseTo"] + paramadd=["channel.getVersion(), responseTo"] + else + rettype="RequestId" + ret="return " + sigadd=[] + paramadd=["channel.getVersion()"] + end + sig=(m.signature+sigadd).join(", ") + params=(paramadd+m.param_names).join(",\n ") + gen <<EOS +#{rettype} #{@classname}::#{cname}::#{m.cppname}(#{sig}) { + #{ret}channel.send(new #{m.body_name}(#{params})); +} + +EOS + end + + def get_decl(c) + cname=c.name.caps + gen " #{cname}& #{@classname}::get#{cname}();\n" + end + + def get_defn(c) + cname=c.name.caps + gen <<EOS +#{@classname}::#{c.name.caps}& #{@classname}::get#{c.name.caps}() +{ + return #{proxy_member(c)}; +} +EOS + end + + def generate + # .h file + h_file(@classname+".h") { + gen <<EOS +#include "qpid/framing/Proxy.h" + +namespace qpid { +namespace framing { + +class #{@classname} : public Proxy +{ +public: + #{@classname}(ChannelAdapter& ch); + + // Inner class definitions +EOS + @amqp.classes.each{ |c| inner_class_decl(c) } + gen " // Inner class instance get methods\n" + @amqp.classes.each{ |c| get_decl(c) } + gen <<EOS + private: + // Inner class instances +EOS + indent { @amqp.classes.each{ |c| gen c.cppname+" "+proxy_member(c)+";\n" } } + gen <<EOS +}; /* class #{@classname} */ + +} /* namespace framing */ +} /* namespace qpid */ +EOS + } + + # .cpp file + cpp_file(@classname+".cpp") { + gen <<EOS +#include <sstream> +#include "#{@classname}.h" +#include "qpid/framing/ChannelAdapter.h" +#include "qpid/framing/amqp_types_full.h" +EOS + @amqp.methods_on(@chassis).each { |m| include(m) } + gen <<EOS +namespace qpid { +namespace framing { + +#{@classname}::#{@classname}(ChannelAdapter& ch) : +EOS + gen " Proxy(ch)" + @amqp.classes.each { |c| gen ",\n "+proxy_member(c)+"(channel)" } + gen <<EOS + {} + + // Inner class instance get methods +EOS + @amqp.classes.each { |c| get_defn(c) } + gen " // Inner class implementation\n\n" + @amqp.classes.each { |c| inner_class_defn(c) } + gen "}} // namespae qpid::framing" + } + end +end + + +ProxyGen.new("client", ARGV[0], Amqp).generate; +ProxyGen.new("server", ARGV[0], Amqp).generate; + diff --git a/cpp/rubygen/samples/runme b/cpp/rubygen/samples/runme new file mode 100755 index 0000000000..e06d128707 --- /dev/null +++ b/cpp/rubygen/samples/runme @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +# +# Run this script to generate code for the C++ broker. +# +$: << ".." # Include .. in load path +if ARGV.size < 2 + puts "Usage: <output_directory> <amqp .xml files...>" + puts "Note: passing '-' as the output directory lists generated files." + exit 1 +end + +require 'amqpgen' +Amqp=AmqpRoot.new(*ARGV[1,ARGV.size]) + +require 'Proxy.rb' +require 'Operations.rb' + + |