cplus2ruby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,210 @@
1
+ ---------------------------------------------------------
2
+ Cplus2Ruby - Gluing C++ and Ruby together in an OO manner
3
+ ---------------------------------------------------------
4
+
5
+ COPYRIGHT
6
+
7
+ Copyright (c) 2007, 2008 by Michael Neumann (mneumann@ntecs.de).
8
+ All rights reserved.
9
+
10
+ LICENSE
11
+
12
+ Ruby License.
13
+
14
+ ABOUT
15
+
16
+ Cplus2Ruby (or "C++Ruby") makes it easy to mix Ruby and C++ in
17
+ a seamless way. You can use the power of the Ruby object model
18
+ and where needed switch to C++ methods for ultimate performance!
19
+
20
+ Cplus2Ruby generates getter and setter methods for C++ properties,
21
+ and wrapper methods so that you can call your C++ methods from
22
+ Ruby without writing a single line of C++ wrapper code. In the same
23
+ way stub methods enable your C++ methods to directly call a Ruby
24
+ method.
25
+
26
+ As mentioned above shortly, the main purpose of Cplus2Ruby is speed.
27
+ Accessing instance variables in Ruby is somewhat slow compared to
28
+ accessing an C++ attribute. Calling a C++ method is as well *a lot*
29
+ faster than calling a Ruby method. Cplus2Ruby now allows you to
30
+ write your performance critical methods in C++, which can call other
31
+ C++ methods and access C++ attributes with native C++ performance.
32
+
33
+ INSTALLATION
34
+
35
+ gem install cplus2ruby
36
+
37
+ DEPENDENCIES
38
+
39
+ * gem install facets
40
+ * C++ compiler and make
41
+
42
+ EXAMPLE
43
+
44
+ Take a look at the following example. You should also take a look
45
+ at the generated C++ source file (work/*.cc). Note that properties
46
+ are actually members of a C++ class, not instance variables, and as
47
+ such, their access from C++ is very fast. As calling a method is
48
+ quite slow in Ruby, a method defined in C++ ("method") can be called
49
+ directly from C++, which again is very fast!
50
+
51
+ $LOAD_PATH.unshift './lib'
52
+ require 'rubygems'
53
+ require 'cplus2ruby'
54
+
55
+ class NeuralEntity; cplus2ruby
56
+ property :id
57
+ end
58
+
59
+ class Neuron < NeuralEntity
60
+ property :potential, :float
61
+ property :last_spike_time, :float
62
+ property :pre_synapses
63
+
64
+ method :stimulate, {:at => :float},{:weight => :float}, %{
65
+ // This is C++ Code
66
+ @potential += at*weight;
67
+
68
+ // call a Ruby method
69
+ log(@potential);
70
+ }
71
+
72
+ stub_method :log, {:pot => :float}
73
+
74
+ def log(pot)
75
+ puts "log(#{pot})"
76
+ end
77
+
78
+ def initialize
79
+ self.pre_synapses = []
80
+ end
81
+ end
82
+
83
+ if __FILE__ == $0
84
+ #
85
+ # Generate C++ code, compile and load shared library.
86
+ #
87
+ Cplus2Ruby.startup('work/neural')
88
+
89
+ n = Neuron.new
90
+ n.id = "n1"
91
+ n.potential = 1.0
92
+ n.stimulate(1.0, 2.0)
93
+ p n.potential # => 3.0
94
+ end
95
+
96
+ FEATURES
97
+
98
+ You can disable the substitution of "@" to "this->" in the generated
99
+ C++ source code with:
100
+
101
+ Cplus2Ruby.settings :substitute_iv_ats => false
102
+
103
+ A method signature to return a value (in our case an integer) looks like:
104
+
105
+ method :abc, {:arg1 => :int}, {:arg2 => :float}, {:returns => :int}, %{
106
+ ...
107
+ }
108
+
109
+ Mixins can be used:
110
+
111
+ module Mixin; cplus2ruby
112
+ property :a
113
+ end
114
+
115
+ class C; cplus2ruby
116
+ include Mixin
117
+ end
118
+
119
+ They don't generate a C++ class, instead get inlined into the class
120
+ into which they are mixed in.
121
+
122
+ You can use type aliases:
123
+
124
+ Cplus2Ruby.add_type_alias 'MyIntegerType' => 'unsigned int'
125
+
126
+ After that Cplus2Ruby knows about this type and how to convert it
127
+ (if it knows how to convert the 'unsigned int' type) and you can
128
+ use it wherever you want.
129
+
130
+ Inline, static and virtual methods can be declared like this:
131
+
132
+ method :abc, {:a1 => :int}, %{
133
+ body
134
+ }, :inline => true, :static => true, :virtual => true
135
+
136
+ There is also a static_method short-cut for static methods, so instead
137
+ of:
138
+
139
+ method :abc, {:a1 => :int}, %{
140
+ ...
141
+ }, :static => true
142
+
143
+ you can write:
144
+
145
+ static_method :abc, {:a1 => :int}, %{
146
+ ...
147
+ }
148
+
149
+ To mark a method in a class hierarchy forever as virtual, you can
150
+ write:
151
+
152
+ virtual :method1, :method2
153
+
154
+ You can also define a class that is purely used from within C++.
155
+ If you don't want to generate wrapper code etc. specify:
156
+
157
+ cplus2ruby :no_wrap => true
158
+
159
+ You can use Strings, Symbols and Classes for types in signatures or
160
+ in property declarations. There is no distinction between Strings and
161
+ Symbols. If you specify a class, it must be known to Cplus2Ruby,
162
+ either explicitly:
163
+
164
+ class A
165
+ cplus2ruby # marks it known to Cplus2Ruby
166
+ end
167
+
168
+ Or using inheritance:
169
+
170
+ class A
171
+ cplus2ruby # marks it known to Cplus2Ruby
172
+ end
173
+
174
+ class B < A # implicit by inheritance
175
+ end
176
+
177
+ Global code (mostly type declarations etc.) can be added as shown
178
+ below:
179
+
180
+ Cplus2Ruby << %q{
181
+ #include <assert.h>
182
+ #include <math.h>
183
+
184
+ #define real_exp expf
185
+ #define real_fabs fabsf
186
+
187
+ #define THROW(str) rb_raise(rb_eRuntimeError, str)
188
+ }
189
+
190
+ Compilation flags etc.:
191
+
192
+ Cplus2Ruby.startup(module_name, force_compilation, cflags, ldflags)
193
+
194
+ For example:
195
+
196
+ #
197
+ # force_compilation => true regenerates and recompiles the
198
+ # C++ code every time.
199
+ #
200
+ Cplus2Ruby.startup("work/mymodule", true, '-DNDEBUG -Winline -Wall', '-lm')
201
+
202
+ BUGS
203
+
204
+ * I get an "illegal instruction" (sig 4) when the C++ code is compiled
205
+ with -pthread. This is the default in the ports on FreeBSD 7.0 even
206
+ when WITH_PTHREAD is defined. It is somehow related to the GC,
207
+ because when I disable the GC everything is fine (except memory
208
+ usage :).
209
+
210
+ END
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = "cplus2ruby"
5
+ s.version = "1.0.0"
6
+ s.summary = "Gluing C++ and Ruby together in an OO manner"
7
+ s.files = Dir['**/*']
8
+ s.add_dependency('facets', '>= 2.3.0')
9
+
10
+ s.author = "Michael Neumann"
11
+ s.email = "mneumann@ntecs.de"
12
+ s.homepage = "http://www.ntecs.de/projects/cplus2ruby/"
13
+ s.rubyforge_project = "cplus2ruby"
14
+ end
15
+
16
+ if __FILE__ == $0
17
+ Gem::manage_gems
18
+ Gem::Builder.new(spec).build
19
+ end
data/example.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ $LOAD_PATH.unshift './lib'
3
+ require 'cplus2ruby'
4
+
5
+ class NeuralEntity; cplus2ruby
6
+ property :id
7
+ end
8
+
9
+ class Neuron < NeuralEntity
10
+ property :potential, :float
11
+ property :last_spike_time, :float
12
+ property :pre_synapses
13
+
14
+ method :stimulate, {:at => :float},{:weight => :float}, %{
15
+ // This is C++ Code
16
+ @potential += at*weight;
17
+
18
+ // call a Ruby method
19
+ log(@potential);
20
+ }
21
+
22
+ stub_method :log, {:pot => :float}
23
+
24
+ def log(pot)
25
+ puts "log(#{pot})"
26
+ end
27
+
28
+ def initialize
29
+ self.pre_synapses = []
30
+ end
31
+ end
32
+
33
+ if __FILE__ == $0
34
+ #
35
+ # Generate C++ code, compile and load shared library.
36
+ #
37
+ Cplus2Ruby.startup('work/neural')
38
+
39
+ n = Neuron.new
40
+ n.id = "n1"
41
+ n.potential = 1.0
42
+ n.stimulate(1.0, 2.0)
43
+ p n.potential # => 3.0
44
+ end
@@ -0,0 +1,52 @@
1
+ class Cplus2Ruby::CodeGenerator
2
+ require 'fileutils'
3
+
4
+ def initialize(model=Cplus2Ruby.model)
5
+ @model = model
6
+ end
7
+
8
+ #
9
+ # Allows preprocessing of generated code.
10
+ #
11
+ def write_out(file, str)
12
+ if @model.settings()[:substitute_iv_ats]
13
+ str.gsub!('@', 'this->')
14
+ end
15
+ FileUtils.mkdir_p(File.dirname(file))
16
+ File.open(file, 'w+') {|out| out.puts str}
17
+ end
18
+
19
+ def no_wrap?(klass)
20
+ (klass.local_annotations[:__options__] || {})[:no_wrap]
21
+ end
22
+
23
+ def wrap?(klass)
24
+ not no_wrap?(klass)
25
+ end
26
+
27
+ def all_properties_of(klass)
28
+ klass.local_annotations.each do |name, options|
29
+ next if options[:class] != Cplus2Ruby::Property
30
+ yield name, options
31
+ end
32
+ end
33
+
34
+ def all_methods_of(klass)
35
+ klass.local_annotations.each do |name, options|
36
+ next if options[:class] != Cplus2Ruby::Method
37
+ yield name, options
38
+ end
39
+ end
40
+
41
+ def args_convertable?(args)
42
+ #FIXME: Facets 2.3.0 has a bug in Dictionary#all?
43
+ ##args.all? {|_, type| @model.typing.can_convert?(type) }
44
+ args.each {|_, type| return false unless @model.typing.can_convert?(type) }
45
+ return true
46
+ end
47
+
48
+ def arity(args)
49
+ args.size - (args.include?(:returns) ? 1 : 0)
50
+ end
51
+
52
+ end
@@ -0,0 +1,84 @@
1
+ class Cplus2Ruby::Compiler
2
+ require 'cplus2ruby/cpp_code_generator'
3
+ require 'cplus2ruby/wrapper_code_generator'
4
+
5
+ def initialize(model=Cplus2Ruby.model)
6
+ @model = model
7
+ end
8
+
9
+ def write_files(mod_name)
10
+ cpp_cg = Cplus2Ruby::CppCodeGenerator.new(@model)
11
+ wrap_cg = Cplus2Ruby::WrapperCodeGenerator.new(@model)
12
+ cpp_cg.write_files(mod_name)
13
+ wrap_cg.write_files(mod_name)
14
+ end
15
+
16
+ def startup(file, force_compilation=false, cflags="", libs="", &block)
17
+ n = names(file)
18
+
19
+ if not force_compilation
20
+ begin
21
+ require n[:ld]
22
+ block.call if block
23
+ return
24
+ rescue LoadError
25
+ end
26
+ end
27
+
28
+ compile(file, cflags, libs)
29
+ require n[:ld]
30
+ block.call if block
31
+ end
32
+
33
+ #
34
+ # Compiles +file+. Returns the name of the shared object to
35
+ # use by +require+.
36
+ #
37
+ def compile(file, cflags="", libs="")
38
+ n = names(file)
39
+
40
+ require 'win32/process' if RUBY_PLATFORM.match('mswin')
41
+ require 'fileutils'
42
+
43
+ FileUtils.mkdir_p(n[:dir])
44
+
45
+ make = RUBY_PLATFORM.match('mswin') ? 'nmake' : 'make'
46
+
47
+ Dir.chdir(n[:dir]) do
48
+ system("#{make} clean") if File.exist?('Makefile')
49
+ write_files(n[:mod])
50
+
51
+ pid = fork do
52
+ require 'mkmf'
53
+ $CFLAGS = cflags
54
+ $LIBS << (" -lstdc++ " + libs)
55
+ create_makefile(n[:mod])
56
+ exec "#{make}"
57
+ end
58
+ _, status = Process.waitpid2(pid)
59
+
60
+ if RUBY_PLATFORM.match('mswin')
61
+ raise if status != 0
62
+ else
63
+ raise if status.exitstatus != 0
64
+ end
65
+ end
66
+
67
+ return n[:ld]
68
+ end
69
+
70
+ def names(file)
71
+ require 'rbconfig'
72
+ base = File.basename(file)
73
+ dir = File.dirname(file)
74
+ mod, ext = base.split(".")
75
+ ld = "#{dir}/#{mod}.#{Config::CONFIG['DLEXT']}"
76
+ { :file => file,
77
+ :base => base,
78
+ :dir => dir,
79
+ :mod => mod,
80
+ :ext => ext,
81
+ :ld => ld }
82
+ end
83
+
84
+ end
@@ -0,0 +1,287 @@
1
+ require 'cplus2ruby/code_generator'
2
+
3
+ class Cplus2Ruby::CppCodeGenerator < Cplus2Ruby::CodeGenerator
4
+ DEFAULT_INCLUDES = [:"stdlib.h", "ruby.h"]
5
+
6
+ def gen_rubyObject
7
+ %[
8
+ struct RubyObject {
9
+ VALUE __obj__;
10
+ RubyObject() { __obj__ = Qnil; }
11
+ virtual ~RubyObject() {};
12
+ static void __free(void *ptr) { ((RubyObject*)ptr)->__free__(); }
13
+ static void __mark(void *ptr) { ((RubyObject*)ptr)->__mark__(); }
14
+ virtual void __free__() { delete this; }
15
+ virtual void __mark__() { }
16
+ };
17
+ ]
18
+ end
19
+
20
+ def gen_include(inc)
21
+ "#include " +
22
+ case inc
23
+ when Symbol
24
+ %{<#{inc}>}
25
+ when String
26
+ %{"#{inc}"}
27
+ else
28
+ raise ArgumentError, "invalid header"
29
+ end
30
+ end
31
+
32
+ def gen_includes(includes)
33
+ includes.map {|inc| gen_include(inc) }.join("\n")
34
+ end
35
+
36
+ def gen_type_alias(from, to)
37
+ "typedef #{to} #{from};"
38
+ end
39
+
40
+ #
41
+ # Type aliases is a hash in the form from => to.
42
+ #
43
+ def gen_type_aliases(type_aliases)
44
+ type_aliases.map {|from, to| gen_type_alias(from, to) }.join("\n")
45
+ end
46
+
47
+ #
48
+ # +kind+ is either :free or :mark
49
+ #
50
+ def gen_free_or_mark_method(klass, kind)
51
+ stmts = stmts_for_free_or_mark_method(klass, kind)
52
+ return "" if stmts.empty?
53
+ stmts << "super::__#{kind}__()"
54
+ %[
55
+ void #{klass.name}::__#{kind}__()
56
+ {
57
+ #{stmts.join(";\n")};
58
+ }
59
+ ]
60
+ end
61
+
62
+ def gen_constructor(klass)
63
+ stmts = []
64
+ all_properties_of(klass) do |name, options|
65
+ init = @model.typing.lookup_entry(:init, options, options[:type])
66
+ stmts << @model.typing.var_assgn("this->#{name}", init) unless init.nil?
67
+ end
68
+ #return "" if stmts.empty?
69
+ %[
70
+ #{klass.name}::#{klass.name}()
71
+ {
72
+ #{stmts.join(";\n")};
73
+ }
74
+ ]
75
+ end
76
+
77
+ def gen_property(name, options)
78
+ @model.typing.var_decl(options[:type], name)
79
+ end
80
+
81
+ #
82
+ # If +klassname+ is nil, then it doesn't include the
83
+ # Klassname:: prefix.
84
+ #
85
+ # Doesn't include the semicolon at the end.
86
+ #
87
+ def gen_method_sig(klassname, name, options, is_declaration)
88
+ args = options[:arguments].dup
89
+ returns = args.delete(:returns) || "void"
90
+
91
+ out = ""
92
+ if is_declaration
93
+ out << "static " if options[:static]
94
+ out << "inline " if options[:inline]
95
+ out << "virtual " if options[:virtual]
96
+ end
97
+ out << @model.typing.var_decl(returns, "")
98
+ out << " "
99
+
100
+ s = args.map {|aname, atype| @model.typing.var_decl(atype, aname) }.join(", ")
101
+
102
+ out << "#{klassname}::" if klassname
103
+ out << "#{name}(#{s})"
104
+ return out
105
+ end
106
+
107
+ def gen_method_body(klassname, name, options)
108
+ if options[:stub]
109
+ gen_stub_method(klassname, name, options)
110
+ else
111
+ "{\n" + (options[:body] || @model.settings[:default_body_when_nil]) + "}\n"
112
+ end
113
+ end
114
+
115
+ #
116
+ # Generates a C++ method that forwards the call to the Ruby method
117
+ # of the same name.
118
+ #
119
+ def gen_stub_method(klassname, name, options)
120
+ raise "Stub method with body is invalid!" if options[:body]
121
+
122
+ args = options[:arguments].dup
123
+ unless args_convertable?(args)
124
+ raise "ERROR: Cannot convert stub method #{klassname}::#{name}"
125
+ end
126
+
127
+ returns = args.delete(:returns) || "void"
128
+
129
+ out = ""
130
+ out << "{\n"
131
+ out << "VALUE __res__ = " if returns != 'void'
132
+
133
+ # TODO: move rb_intern out
134
+ call_args = ["@__obj__", %{rb_intern("#{name}")}, args.size] +
135
+ args.map {|n, k| @model.typing.convert(k, n, :c2ruby) }
136
+
137
+ out << %{rb_funcall(#{call_args.join(', ')});}
138
+
139
+ # check return type
140
+ if returns != 'void'
141
+ out << @model.typing.convert(returns, '__res__', :ruby2c_checktype)
142
+ retval = @model.typing.convert(returns, '__res__', :ruby2c)
143
+ out << "return #{retval};\n"
144
+ end
145
+
146
+ out << "}\n"
147
+
148
+ return out
149
+ end
150
+
151
+ def gen_method(klassname, name, options, include_body, is_declaration)
152
+ str = gen_method_sig(klassname, name, options, is_declaration)
153
+ str << gen_method_body(klassname, name, options) if include_body
154
+ str
155
+ end
156
+
157
+ def gen_class_declaration(klass)
158
+ if klass.superclass == Object
159
+ sc = "RubyObject"
160
+ else
161
+ sc = klass.superclass.name
162
+ end
163
+
164
+ #
165
+ # Do we have free or mark methods defined?
166
+ #
167
+ m = {}
168
+ [:free, :mark].each do |kind|
169
+ if not stmts_for_free_or_mark_method(klass, kind).empty?
170
+ m[kind] = "virtual void __#{kind}__();"
171
+ end
172
+ end
173
+
174
+ #
175
+ # Write out property declarations and method signatures.
176
+ #
177
+ stmts = []
178
+
179
+ all_properties_of(klass) {|name, options|
180
+ stmts << gen_property(name, options)
181
+ }
182
+
183
+ all_methods_of(klass) {|name, options|
184
+ stmts << gen_method(nil, name, options, options[:inline], true)
185
+ }
186
+
187
+ if no_wrap?(klass)
188
+ %[
189
+ struct #{klass.name}
190
+ {
191
+ #{stmts.join("; \n")};
192
+ };
193
+ ]
194
+ else
195
+ %[
196
+ struct #{klass.name} : #{sc}
197
+ {
198
+ typedef #{sc} super;
199
+
200
+ #{klass.name}();
201
+
202
+ #{m[:free]}
203
+ #{m[:mark]}
204
+
205
+ #{stmts.join("; \n")};
206
+ };
207
+ ]
208
+ end
209
+ end
210
+
211
+ def gen_class_impl(klass)
212
+ # FIXME: helper_codes
213
+
214
+ stmts = []
215
+
216
+ if wrap?(klass)
217
+ stmts << gen_constructor(klass)
218
+
219
+ [:free, :mark].each {|kind|
220
+ stmts << gen_free_or_mark_method(klass, kind)
221
+ }
222
+ end
223
+
224
+ all_methods_of(klass) do |name, options|
225
+ next if options[:inline]
226
+ stmts << gen_method(klass.name, name, options, true, false)
227
+ end
228
+
229
+ stmts.join("\n")
230
+ end
231
+
232
+ def gen_header_file
233
+ out = ""
234
+ out << gen_includes(DEFAULT_INCLUDES + @model.includes)
235
+ out << gen_rubyObject()
236
+ out << gen_type_aliases(@model.typing.aliases)
237
+ out << @model.code
238
+
239
+ # forward class declarations
240
+ @model.entities_ordered.each do |klass|
241
+ out << "struct #{klass.name};\n"
242
+ end
243
+
244
+ # FIXME: helper_headers
245
+
246
+ #
247
+ # class declarations
248
+ #
249
+ @model.entities_ordered.each do |klass|
250
+ out << gen_class_declaration(klass)
251
+ end
252
+
253
+ return out
254
+ end
255
+
256
+ def gen_impl_file(mod_name)
257
+ out = ""
258
+ out << %{#include "#{mod_name}.h"\n\n}
259
+
260
+ #
261
+ # class declarations
262
+ #
263
+ @model.entities_ordered.each do |klass|
264
+ out << gen_class_impl(klass)
265
+ end
266
+
267
+ return out
268
+ end
269
+
270
+ def write_files(mod_name)
271
+ write_out(mod_name + ".h", gen_header_file())
272
+ write_out(mod_name + ".cc", gen_impl_file(mod_name))
273
+ end
274
+
275
+ protected
276
+
277
+ def stmts_for_free_or_mark_method(klass, kind)
278
+ stmts = []
279
+ all_properties_of(klass) do |name, options|
280
+ if free_mark = @model.typing.lookup_entry(kind, options, options[:type])
281
+ stmts << free_mark.gsub('%s', "this->#{name}") # FIXME: use a common replacement function
282
+ end
283
+ end
284
+ stmts
285
+ end
286
+
287
+ end