cb 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,42 @@
1
+ Simple native Callback object for Ruby MRI
2
+ (c) 2009 Lourens Naudé (methodmissing), James Tucker (raggi) and coderrr
3
+
4
+ http://github.com/methodmissing/cb
5
+
6
+ This library works with Ruby 1.8 and 1.9 and is a more efficient
7
+ implementation of the following Ruby code :
8
+
9
+ class RubyCallback
10
+ def initialize(object = nil, method = :call, &b)
11
+ @object, @method = object, method
12
+ @object ||= b
13
+ end
14
+
15
+ def call(*args)
16
+ @object.__send__(@method, *args)
17
+ end
18
+ end
19
+
20
+ module Kernel
21
+ private
22
+ def RubyCallback(object = nil, method = :call, &b)
23
+ RubyCallback.new(object, method, &b)
24
+ end
25
+ end
26
+
27
+ Concept, ideas and API design James's - any pointers for better GC integration much appreciated.
28
+
29
+ Examples :
30
+
31
+ 'hai'.callback(:gsub).call('h', 'b') #=> 'bai'
32
+ Callback( 'bai', :to_s ).call #=> 'hai'
33
+ Callback{ 'hai' }.call #=> 'hai'
34
+ Callback( 'bai', :gsub ).call( 'b', 'h' ) #=> 'hai'
35
+
36
+ To run the test suite:
37
+
38
+ rake
39
+
40
+ To run the benchmarks:
41
+
42
+ rake bench
data/Rakefile ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env rake
2
+ require 'rake/testtask'
3
+ require 'rake/clean'
4
+
5
+ def spec(file = Dir['*.gemspec'].first)
6
+ @spec ||=
7
+ begin
8
+ require 'rubygems/specification'
9
+ Thread.abort_on_exception = true
10
+ data = File.read(file)
11
+ spec = nil
12
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
13
+ spec.instance_variable_set(:@filename, file)
14
+ def spec.filename; @filename; end
15
+ spec
16
+ end
17
+ end
18
+
19
+ def manifest; @manifest ||= `git ls-files`.split("\n").reject{|s|s=~/\.gemspec$|\.gitignore$/}; end
20
+
21
+ @gem_package_task_type = begin
22
+ require 'rubygems/package_task'
23
+ Gem::PackageTask
24
+ rescue LoadError
25
+ require 'rake/gempackagetask'
26
+ Rake::GemPackageTask
27
+ end
28
+ def gem_task; @gem_task ||= @gem_package_task_type.new(spec); end
29
+ gem_task.define
30
+ Rake::Task[:clobber].enhance [:clobber_package]
31
+
32
+ rdoc_task_type = begin
33
+ require 'rdoc/task'
34
+ RDoc::Task
35
+ rescue LoadError
36
+ require 'rake/rdoctask'
37
+ Rake::RDocTask
38
+ end
39
+ df = begin; require 'rdoc/generator/darkfish'; true; rescue LoadError; end
40
+ rdtask = rdoc_task_type.new do |rd|
41
+ rd.title = spec.name
42
+ rd.main = spec.extra_rdoc_files.first
43
+ lib_rexp = spec.require_paths.map { |p| Regexp.escape p }.join('|')
44
+ rd.rdoc_files.include(*manifest.grep(/^(?:#{lib_rexp})/))
45
+ rd.rdoc_files.include(*spec.extra_rdoc_files)
46
+ rd.template = 'darkfish' if df
47
+ end
48
+
49
+ Rake::Task[:clobber].enhance [:clobber_rdoc]
50
+
51
+ desc 'Generate and open documentation'
52
+ task :docs => :rdoc do
53
+ path = rdtask.send :rdoc_target
54
+ case RUBY_PLATFORM
55
+ when /darwin/ ; sh "open #{path}"
56
+ when /mswin|mingw/ ; sh "start #{path}"
57
+ else
58
+ sh "firefox #{path}"
59
+ end
60
+ end
61
+
62
+ desc "Regenerate gemspec"
63
+ task :gemspec => spec.filename
64
+
65
+ task spec.filename do
66
+ spec.files = manifest
67
+ spec.test_files = FileList['{test,spec}/**/{test,spec}_*.rb']
68
+ open(spec.filename, 'w') { |w| w.write spec.to_ruby }
69
+ end
70
+
71
+ desc "Bump version from #{spec.version} to #{spec.version.to_s.succ}"
72
+ task :bump do
73
+ spec.version = spec.version.to_s.succ
74
+ end
75
+
76
+ desc "Tag version #{spec.version}"
77
+ task :tag do
78
+ tagged = Dir.new('.git/refs/tags').entries.include? spec.version.to_s
79
+ if tagged
80
+ warn "Tag #{spec.version} already exists"
81
+ else
82
+ # TODO release message in tag message
83
+ sh "git tag #{spec.version}"
84
+ end
85
+ end
86
+
87
+ if spec.rubyforge_project
88
+ require 'yaml'
89
+ require 'rake/contrib/sshpublisher'
90
+ desc "Publish rdoc to rubyforge"
91
+ task :publish => rdtask.name do
92
+ rf_cfg = File.expand_path '~/.rubyforge/user-config.yml'
93
+ host = "#{YAML.load_file(rf_cfg)['username']}@rubyforge.org"
94
+ remote_dir = "/var/www/gforge-projects/#{spec.rubyforge_project}/#{spec.name}/"
95
+ Rake::SshDirPublisher.new(host, remote_dir, rdtask.rdoc_dir).upload
96
+ end
97
+
98
+ desc "Release #{gem_task.gem_file} to rubyforge"
99
+ task :release => [:tag, :gem, :publish] do |t|
100
+ sh "rubyforge add_release #{spec.rubyforge_project} #{spec.name} #{spec.version} #{gem_task.package_dir}/#{gem_task.gem_file}"
101
+ end
102
+ else
103
+ warn "Cannot release to rubyforge - no rubyforge_project in gemspec"
104
+ end
105
+
106
+ CB_ROOT = 'ext/mri_callback'
107
+
108
+ desc 'Default: test'
109
+ task :default => :test
110
+
111
+ desc 'Run callback tests.'
112
+ Rake::TestTask.new(:test) do |t|
113
+ t.libs << CB_ROOT
114
+ t.pattern = 'test/test_*.rb'
115
+ t.verbose = true
116
+ end
117
+ task :test
118
+
119
+ namespace :build do
120
+ file "#{CB_ROOT}/mri_callback.c"
121
+ file "#{CB_ROOT}/extconf.rb"
122
+ file "#{CB_ROOT}/Makefile" => %W(#{CB_ROOT}/mri_callback.c #{CB_ROOT}/extconf.rb) do
123
+ Dir.chdir(CB_ROOT) do
124
+ ruby 'extconf.rb'
125
+ end
126
+ end
127
+
128
+ desc "generate makefile"
129
+ task :makefile => %W(#{CB_ROOT}/Makefile #{CB_ROOT}/mri_callback.c)
130
+
131
+ dlext = Config::CONFIG['DLEXT']
132
+ file "#{CB_ROOT}/mri_callback.#{dlext}" => %W(#{CB_ROOT}/Makefile #{CB_ROOT}/mri_callback.c) do
133
+ Dir.chdir(CB_ROOT) do
134
+ sh 'make' # TODO - is there a config for which make somewhere?
135
+ end
136
+ end
137
+
138
+ desc "compile callback extension"
139
+ task :compile => "#{CB_ROOT}/mri_callback.#{dlext}"
140
+
141
+ task :clean do
142
+ Dir.chdir(CB_ROOT) do
143
+ sh 'make clean'
144
+ end if File.exists?("#{CB_ROOT}/Makefile")
145
+ end
146
+
147
+ CLEAN.include("#{CB_ROOT}/Makefile")
148
+ CLEAN.include("#{CB_ROOT}/mri_callback.#{dlext}")
149
+ end
150
+
151
+ task :clean => %w(build:clean)
152
+
153
+ desc "compile"
154
+ task :build => %w(build:compile)
155
+
156
+ task :install do |t|
157
+ Dir.chdir(CB_ROOT) do
158
+ sh 'sudo make install'
159
+ end
160
+ end
161
+
162
+ desc "clean build install"
163
+ task :setup => %w(clean build install)
164
+
165
+ desc "run benchmarks"
166
+ task :bench do |t|
167
+ ruby "-Ilib:ext/mri_callback:bench", "bench/bench.rb"
168
+ puts
169
+ ruby "-Ilib:ext/mri_callback:bench", "bench/raggi_bench.rb"
170
+ end
171
+ task :bench
data/bench/bench.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "benchmark"
2
+ require "callback"
3
+ require "pure_ruby_cb"
4
+
5
+ class Object
6
+ public :method
7
+ end
8
+
9
+ def meth
10
+ 'hai'
11
+ end
12
+
13
+ TESTS = 10_000
14
+ Benchmark.bmbm do |results|
15
+ results.report("Method object:") { TESTS.times { method(:meth).call } }
16
+ results.report("Callback{ 'hai' }.call") { TESTS.times { Callback{ 'hai' }.call } }
17
+ results.report("RubyCallback{ 'hai' }.call") { TESTS.times { RubyCallback{ 'hai' }.call } }
18
+ results.report("Callback( 'bai', :to_s ).call") { TESTS.times { Callback( 'bai', :to_s ).call } }
19
+ results.report("RubyCallback( 'bai', :to_s ).call") { TESTS.times { RubyCallback( 'bai', :to_s ).call } }
20
+ results.report("Callback( 'bai', :gsub ).call( 'b', 'h' )") { TESTS.times { Callback( 'bai', :gsub ).call( 'b', 'h' ) } }
21
+ results.report("RubyCallback( 'bai', :gsub ).call( 'b', 'h' )") { TESTS.times { RubyCallback( 'bai', :gsub).call( 'b', 'h' ) } }
22
+ end
@@ -0,0 +1,17 @@
1
+ class RubyCallback
2
+ def initialize(object = nil, method = :call, &b)
3
+ @object, @method = object, method
4
+ @object ||= b
5
+ end
6
+
7
+ def call(*args)
8
+ @object.__send__(@method, *args)
9
+ end
10
+ end
11
+
12
+ module Kernel
13
+ private
14
+ def RubyCallback(object = nil, method = :call, &b)
15
+ RubyCallback.new(object, method, &b)
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ require "benchmark"
2
+ require "callback"
3
+ require "pure_ruby_cb"
4
+
5
+ @s = 'foo'
6
+ def me; @s.to_s; end
7
+ ld = lambda { @s.to_s }
8
+ cb = Callback &ld
9
+ c2 = Callback( @s, :to_s )
10
+ rb = RubyCallback { @s.to_s }
11
+ farm = @s.callback(:to_s)
12
+ meth = @s.send(:method, :to_s)
13
+
14
+ tests = 100_000
15
+ puts "# Calling"
16
+ Benchmark.bmbm do |results|
17
+ results.report("method invoked:") { tests.times { me } }
18
+ results.report("ruby method obj:") { tests.times { meth.call } }
19
+ results.report("object native callback:") { tests.times { farm.call } }
20
+ results.report("callback native:") { tests.times { c2.call } }
21
+ results.report("callback block:") { tests.times { cb.call } }
22
+ results.report("pure ruby callback:") { tests.times { rb.call } }
23
+ results.report("lambda:") { tests.times { ld.call } }
24
+ end
@@ -0,0 +1,5 @@
1
+ require 'mkmf'
2
+
3
+ dir_config('mri_callback')
4
+
5
+ create_makefile('mri_callback')
@@ -0,0 +1,85 @@
1
+ #include "ruby.h"
2
+
3
+ #ifndef RSTRING_PTR
4
+ #define RSTRING_PTR(obj) RSTRING(obj)->ptr
5
+ #endif
6
+
7
+ typedef struct {
8
+ VALUE object;
9
+ ID method;
10
+ } RCallback;
11
+
12
+ #define GetCallbackStruct(obj) (Check_Type(obj, T_DATA), (RCallback*)DATA_PTR(obj))
13
+
14
+ VALUE rb_cCallback;
15
+ static ID id_call;
16
+
17
+ static void mark_mri_callback(RCallback* cb)
18
+ {
19
+ rb_gc_mark(cb->object);
20
+ }
21
+
22
+ static void free_mri_callback(RCallback* cb)
23
+ {
24
+ xfree(cb);
25
+ }
26
+
27
+ static VALUE callback_alloc _((VALUE));
28
+ static VALUE
29
+ callback_alloc( VALUE klass )
30
+ {
31
+ VALUE cb;
32
+ RCallback* cbs;
33
+ cb = Data_Make_Struct(klass, RCallback, mark_mri_callback, free_mri_callback, cbs);
34
+ cbs->object = Qnil;
35
+ cbs->method = 0;
36
+
37
+ return cb;
38
+ }
39
+
40
+ static VALUE
41
+ rb_mri_callback_new()
42
+ {
43
+ return callback_alloc(rb_cCallback);
44
+ }
45
+
46
+ static VALUE rb_mri_callback_initialize( int argc, VALUE *argv, VALUE cb )
47
+ {
48
+ VALUE object;
49
+ VALUE method;
50
+ ID meth;
51
+ RCallback* cbs = GetCallbackStruct(cb);
52
+
53
+ if (rb_block_given_p()) {
54
+ if (argc == 1) rb_raise(rb_eArgError, "wrong number of arguments");
55
+ cbs->object = rb_block_proc();
56
+ cbs->method = id_call;
57
+ }else {
58
+ rb_scan_args(argc, argv, "02", &object, &method);
59
+ cbs->object = object;
60
+ meth = rb_to_id(method);
61
+ if (!rb_respond_to(object,meth)) rb_raise(rb_eArgError, "object does not respond to %s", RSTRING_PTR(rb_obj_as_string(method)));
62
+ cbs->method = meth;
63
+ }
64
+
65
+ OBJ_FREEZE(cb);
66
+ return cb;
67
+ }
68
+
69
+ static VALUE rb_mri_callback_call( VALUE cb, VALUE args )
70
+ {
71
+ RCallback* cbs = GetCallbackStruct(cb);
72
+ return rb_apply(cbs->object, cbs->method, args);
73
+ }
74
+
75
+ void
76
+ Init_mri_callback()
77
+ {
78
+ id_call = rb_intern("call");
79
+
80
+ rb_cCallback = rb_define_class("Callback", rb_cObject);
81
+ rb_define_alloc_func(rb_cCallback, callback_alloc);
82
+
83
+ rb_define_method(rb_cCallback,"initialize", rb_mri_callback_initialize, -1);
84
+ rb_define_method(rb_cCallback,"call", rb_mri_callback_call, -2);
85
+ }
data/lib/cb.rb ADDED
@@ -0,0 +1,72 @@
1
+ begin
2
+ require 'mri_callback'
3
+ rescue LoadError
4
+ end
5
+
6
+ # A simple __send__ based callback implementation, simply Object Orients
7
+ # something equivalent to:
8
+ #
9
+ # lambda { |*args| object.__send__ method, *args }
10
+ class Callback
11
+ class << self; attr_reader :extension_loaded; end
12
+
13
+ begin
14
+ cb = new(self, :instance_variable_set)
15
+ cb.respond_to?(:call) && cb.call(:@extension_loaded, true)
16
+ rescue ArgumentError
17
+ @extension_loaded = false
18
+ end
19
+
20
+ unless @extension_loaded
21
+ warn "Using pure ruby Callback"
22
+ def initialize(object, method)
23
+ @object, @method = object, method
24
+ end
25
+
26
+ def call(*args)
27
+ @object.__send__(@method, *args)
28
+ end
29
+ end
30
+ end
31
+
32
+ # A quick monkey patch providing a public equivalent of the #method private
33
+ # method aliased to #callback. This is intended for use as a persistent
34
+ # callback object. It should be noted that using Method objects under MRI has
35
+ # in the past lead to user induced leaks. One must be careful when using it.
36
+ class Object
37
+ alias callback method
38
+
39
+ # Allow users to call object.callback(:method_name) to grab a Method object.
40
+ public :callback
41
+ end
42
+
43
+ # Utility method for coercing arguments to an object that responds to #call
44
+ # Accepts an object and a method name to send to, or a block, or an object
45
+ # that responds to call.
46
+ #
47
+ # cb = Callback{ |msg| puts(msg) }
48
+ # cb.call('hello world')
49
+ #
50
+ # cb = Callback(Object, :puts)
51
+ # cb.call('hello world')
52
+ #
53
+ # cb = Callback(proc{ |msg| puts(msg) })
54
+ # cb.call('hello world')
55
+ module Kernel
56
+ def Callback(object = nil, method = nil, &blk)
57
+ if object && method
58
+ Callback.new(object, method)
59
+ else
60
+ if object.respond_to? :call
61
+ object
62
+ else
63
+ if object && blk || blk.nil?
64
+ raise(ArgumentError, "Callback required an object responding to #call or an object and a method, or a block")
65
+ else
66
+ blk
67
+ end
68
+ end
69
+ end
70
+ end
71
+ private :Callback
72
+ end
data/test/test_cb.rb ADDED
@@ -0,0 +1,78 @@
1
+ require "test/unit"
2
+ require "cb"
3
+
4
+ class TestCallbackPureRuby < Test::Unit::TestCase
5
+ def test_Callback_with_lambda
6
+ ran = false
7
+ l = lambda { ran = true }
8
+ Callback(l).call
9
+ assert ran
10
+ end
11
+
12
+ def cb
13
+ @ran = true
14
+ end
15
+
16
+ def test_Callback_with_object_and_method
17
+ @ran = false
18
+ Callback(self, :cb).call
19
+ assert @ran
20
+ end
21
+
22
+ def test_Callback_with_block
23
+ ran = false
24
+ Callback { ran = true }.call
25
+ assert ran
26
+ end
27
+
28
+ def test_Callback_with_no_arguments
29
+ assert_raises(ArgumentError) do
30
+ Callback()
31
+ end
32
+ end
33
+
34
+ def test_Object_callback
35
+ @ran = false
36
+ self.callback(:cb).call
37
+ assert @ran
38
+ end
39
+
40
+ def test_block
41
+ cb = Callback{ 'hai' }
42
+ assert_equal 'hai', cb.call
43
+ end
44
+
45
+ def test_init_object_call_with_no_args
46
+ cb = Callback( 'bai', :to_s )
47
+ assert_equal 'bai', cb.call
48
+ end
49
+
50
+ def test_init_object_call_with_args
51
+ cb = Callback( 'bai', :gsub )
52
+ assert_equal 'hai', cb.call( 'b', 'h' )
53
+ end
54
+
55
+ def test_many_calls
56
+ cb = Callback { :foo }
57
+ 100_000.times { cb.call }
58
+ end
59
+
60
+ def test_arguments
61
+ assert_raises ArgumentError do
62
+ Callback( 'hai' ){}
63
+ end
64
+ end
65
+
66
+ def test_farmed_from_object
67
+ cb = 'hai'.callback(:to_s)
68
+ assert_equal 'hai', cb.call
69
+ ocb = 'bai'.callback(:gsub)
70
+ assert_equal 'hai', ocb.call('b', 'h')
71
+ end
72
+
73
+ def test_invalid_method
74
+ assert_raises NameError do
75
+ 'str'.callback(:undefined)
76
+ end
77
+ end
78
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - "Lourens Naud\xC3\xA9 (methodmissing)"
8
+ - James Tucker (raggi)
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-08-22 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Simple native Callback object for Ruby MRI (1.8.{6,7} and 1.9.2)
18
+ email: lourens@methodmissing.com
19
+ executables: []
20
+
21
+ extensions:
22
+ - ext/mri_callback/extconf.rb
23
+ extra_rdoc_files:
24
+ - README
25
+ files:
26
+ - README
27
+ - Rakefile
28
+ - bench/bench.rb
29
+ - bench/pure_ruby_cb.rb
30
+ - bench/raggi_bench.rb
31
+ - ext/mri_callback/extconf.rb
32
+ - ext/mri_callback/mri_callback.c
33
+ - lib/cb.rb
34
+ - test/test_cb.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/methodmissing/cb
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --main
42
+ - README
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project:
60
+ rubygems_version: 1.3.5
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: Native MRI callback
64
+ test_files:
65
+ - test/test_cb.rb