├── VERSION.yml ├── lib ├── erlectricity │ ├── types │ │ ├── list.rb │ │ ├── pid.rb │ │ ├── reference.rb │ │ ├── new_reference.rb │ │ ├── function.rb │ │ └── new_function.rb │ ├── errors │ │ ├── decode_error.rb │ │ ├── encode_error.rb │ │ └── erlectricity_error.rb │ ├── version.rb │ ├── conditions │ │ ├── boolean.rb │ │ ├── type.rb │ │ ├── hash.rb │ │ └── static.rb │ ├── matcher.rb │ ├── constants.rb │ ├── port.rb │ ├── condition.rb │ ├── receiver.rb │ ├── encoder.rb │ └── decoder.rb └── erlectricity.rb ├── .gitignore ├── test ├── spec_suite.rb ├── test_helper.rb ├── port_spec.rb ├── receiver_spec.rb ├── matcher_spec.rb ├── condition_spec.rb ├── decode_spec.rb └── encode_spec.rb ├── examples ├── simple │ ├── README.md │ ├── rerl.sh │ └── rerl.rb ├── echo │ ├── echo.rb │ ├── README.md │ └── echo.erl ├── gruff │ ├── gruff_run.sh │ ├── stat_run.sh │ ├── gruff_provider.rb │ ├── stat_writer.erl │ └── gruff.erl └── tinderl │ ├── README.md │ ├── tinderl.rb │ └── tinderl.erl ├── ext ├── extconf.rb └── decoder.c ├── benchmarks └── bench.rb ├── LICENSE ├── History.txt ├── Rakefile ├── README.md └── erlectricity.gemspec /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :minor: 1 3 | :patch: 1 4 | :major: 1 5 | -------------------------------------------------------------------------------- /lib/erlectricity/types/list.rb: -------------------------------------------------------------------------------- 1 | class Erlectricity::List < Array 2 | 3 | end -------------------------------------------------------------------------------- /lib/erlectricity/errors/decode_error.rb: -------------------------------------------------------------------------------- 1 | class DecodeError < ErlectricityError 2 | 3 | end -------------------------------------------------------------------------------- /lib/erlectricity/errors/encode_error.rb: -------------------------------------------------------------------------------- 1 | class EncodeError < ErlectricityError 2 | 3 | end -------------------------------------------------------------------------------- /lib/erlectricity/errors/erlectricity_error.rb: -------------------------------------------------------------------------------- 1 | class ErlectricityError < StandardError 2 | 3 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | ext/Makefile 3 | ext/decoder.bundle 4 | ext/decoder.o 5 | *.beam 6 | *.dump 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /lib/erlectricity/types/pid.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | Pid = Struct.new :node, :id, :serial, :creation 3 | end -------------------------------------------------------------------------------- /lib/erlectricity/types/reference.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | Reference = Struct.new :node, :id, :creator 3 | end -------------------------------------------------------------------------------- /lib/erlectricity/types/new_reference.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | NewReference = Struct.new :node, :creation, :id 3 | end -------------------------------------------------------------------------------- /lib/erlectricity/types/function.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | Function = Struct.new :pid, :module, :index, :uniq, :free_vars 3 | end -------------------------------------------------------------------------------- /test/spec_suite.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper.rb' 2 | Dir[File.dirname(__FILE__) + '/*_spec.rb'].each{ |f| require f} -------------------------------------------------------------------------------- /lib/erlectricity/types/new_function.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | NewFunction = Struct.new :arity, :uniq, :index,:num_free, :module, :old_index, :old_uniq, :pid, :free_vars 3 | end -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | This example demonstrates how Erlang and Ruby data types get converted when 2 | crossing from one to the other. 3 | 4 | $ cd examples/simple 5 | $ ./rerl.sh -------------------------------------------------------------------------------- /lib/erlectricity/version.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity #:nodoc: 2 | module VERSION #:nodoc: 3 | MAJOR = 0 4 | MINOR = 2 5 | TINY = 1 6 | 7 | STRING = [MAJOR, MINOR, TINY].join('.') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | # Loads mkmf which is used to make makefiles for Ruby extensions 2 | require 'mkmf' 3 | 4 | # Give it a name 5 | extension_name = 'decoder' 6 | 7 | # The destination 8 | dir_config(extension_name) 9 | 10 | # Do the work 11 | create_makefile(extension_name) -------------------------------------------------------------------------------- /lib/erlectricity/conditions/boolean.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class BooleanCondition < Condition 3 | def satisfies?(arg) 4 | [TrueClass, FalseClass].include?(arg.class) 5 | end 6 | 7 | def binding_for(arg) 8 | arg 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /examples/echo/echo.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), *%w[../../lib]) 2 | 3 | require 'rubygems' 4 | require 'erlectricity' 5 | 6 | receive do |f| 7 | f.when([:echo, String]) do |text| 8 | f.send!([:result, "You said: #{text}"]) 9 | f.receive_loop 10 | end 11 | end -------------------------------------------------------------------------------- /lib/erlectricity/conditions/type.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class TypeCondition < Condition 3 | attr_accessor :type 4 | 5 | def initialize(type) 6 | self.type = type 7 | end 8 | 9 | def satisfies?(arg) 10 | arg.is_a?(self.type) 11 | end 12 | 13 | def binding_for(arg) 14 | arg 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/erlectricity/conditions/hash.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class HashCondition < Condition 3 | def satisfies?(arg) 4 | return false unless arg.class == Array 5 | arg.all? { |x| x.class == Array && x.length == 2 } 6 | end 7 | 8 | def binding_for(arg) 9 | flattened = arg.inject([]) { |memo, kv| memo + kv } 10 | Hash[*flattened] 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /examples/echo/README.md: -------------------------------------------------------------------------------- 1 | This is a simple echo server example showing off basic Erlectricity usage. 2 | 3 | $ cd examples/echo 4 | $ erl 5 | Erlang (BEAM) emulator version 5.6.4 [source] [smp:2] [async-threads:0] [kernel-poll:false] 6 | 7 | Eshell V5.6.4 (abort with ^G) 8 | 1> c(echo). 9 | {ok,echo} 10 | 2> echo:test(). 11 | <<"You said: hello world!">> 12 | ok 13 | -------------------------------------------------------------------------------- /examples/echo/echo.erl: -------------------------------------------------------------------------------- 1 | -module(echo). 2 | -export([test/0]). 3 | 4 | test() -> 5 | Cmd = "ruby echo.rb", 6 | Port = open_port({spawn, Cmd}, [{packet, 4}, nouse_stdio, exit_status, binary]), 7 | Payload = term_to_binary({echo, <<"hello world!">>}), 8 | port_command(Port, Payload), 9 | receive 10 | {Port, {data, Data}} -> 11 | {result, Text} = binary_to_term(Data), 12 | io:format("~p~n", [Text]) 13 | end. -------------------------------------------------------------------------------- /lib/erlectricity/matcher.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class Matcher 3 | attr_accessor :condition, :block 4 | attr_accessor :receiver 5 | 6 | def initialize(parent, condition, block) 7 | self.receiver = parent 8 | @block = block 9 | @condition = Condition.for(condition) 10 | end 11 | 12 | def run(arg) 13 | args = @condition.binding_for(arg) 14 | block.call(*args) 15 | end 16 | 17 | def matches?(arg) 18 | @condition.satisfies?(arg) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/gruff/gruff_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | -export([main/1]). 4 | 5 | main(_Any) -> 6 | Data = [ 7 | {apples, [10, 2, 3, 4, 4, 3]}, 8 | {oranges, [4, 8, 7, 9, 8, 9]}, 9 | {watermelons, [2, 3, 1, 5, 6, 8]}, 10 | {peaches, [9, 9, 10, 8, 7, 9]} 11 | ], 12 | gruff:start(), 13 | Result = gruff:plot( 14 | <<"My Charts">>, 15 | <<"/Users/scott/Library/Fonts/Arial">>, 16 | Data, 17 | [{0, <<"2003">>}, {2, <<"2004">>}, {4, <<"2005">>}] 18 | ), 19 | file:write_file("out.png", Result). -------------------------------------------------------------------------------- /examples/tinderl/README.md: -------------------------------------------------------------------------------- 1 | This is a more advanced example of Erlectricity that shows how you can 2 | integrate with Campfire via the Ruby "tinder" gem. 3 | 4 | $ cd examples/tinderl 5 | $ erl 6 | Erlang (BEAM) emulator version 5.6.4 [source] [smp:2] [async-threads:0] [kernel-poll:false] 7 | 8 | Eshell V5.6.4 (abort with ^G) 9 | 1> c(tinderl). 10 | {ok,tinderl} 11 | 2> tinderl:start("example.campfireapp.com", "tom@example.com", "secret", "My Room"). 12 | <0.38.0> 13 | 5> tinderl:speak("Hello, World!"). 14 | {speak,<0.31.0>,<<"Hello, World!">>} 15 | -------------------------------------------------------------------------------- /examples/gruff/stat_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | -export([main/1]). 4 | 5 | main(_Any) -> 6 | gruff:start(), 7 | MemoryWriter = stat_writer:start(<<"Memory Info">>, fun() -> erlang:memory() end), 8 | ProcessWriter = stat_writer:start(<<"Process Info">>, 9 | fun() -> 10 | {_, QueueLength} = erlang:process_info(erlang:whereis(gruff), message_queue_len), 11 | [{processes, erlang:system_info(process_count)}, 12 | {gruff_queue_length, QueueLength}] 13 | end 14 | ), 15 | receive 16 | after 20000 -> 17 | MemoryWriter ! stop, 18 | ProcessWriter ! stop, 19 | elang:halt() 20 | end. -------------------------------------------------------------------------------- /benchmarks/bench.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + '/../lib') 2 | 3 | require 'erlectricity' 4 | require 'benchmark' 5 | 6 | data = [:ok, [:foo, :bar, [99, "bottles", "of", "beer", 3.14], [true, false]]] 7 | bert = Erlectricity::Encoder.encode(data) 8 | 9 | p bert 10 | 11 | Benchmark.bm do|b| 12 | b.report("Decoder") do 13 | 100_000.times { Erl::Decoder.decode(bert) } 14 | end 15 | end 16 | 17 | # user system total real 18 | # C Decoder 0.400000 0.000000 0.400000 ( 0.425373) 19 | # Ruby Decoder 30.250000 0.220000 30.470000 ( 32.140890) 20 | # 21 | # C decoder is 75.56x faster than Ruby decoder on this data -------------------------------------------------------------------------------- /examples/tinderl/tinderl.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), *%w[../../lib]) 2 | 3 | require 'rubygems' 4 | require 'erlectricity' 5 | require 'tinder' 6 | 7 | domain, email, password, room_name = *ARGV 8 | campfire = Tinder::Campfire.new domain 9 | campfire.login email, password 10 | room = campfire.find_room_by_name room_name 11 | 12 | receive do |f| 13 | f.when([:speak, Any]) do |comment| 14 | room.speak(comment) 15 | f.receive_loop 16 | end 17 | 18 | f.when([:paste, Any]) do |comment| 19 | room.paste(comment) 20 | f.receive_loop 21 | end 22 | 23 | f.when(Any) do |obj| 24 | p obj 25 | end 26 | end 27 | 28 | room.leave if room 29 | -------------------------------------------------------------------------------- /lib/erlectricity/constants.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | module External 3 | module Types 4 | SMALL_INT = 97 5 | INT = 98 6 | 7 | SMALL_BIGNUM = 110 8 | LARGE_BIGNUM = 111 9 | 10 | FLOAT = 99 11 | 12 | ATOM = 100 13 | REF = 101 #old style reference 14 | NEW_REF = 114 15 | PORT = 102 #not supported accross node boundaries 16 | PID = 103 17 | 18 | SMALL_TUPLE = 104 19 | LARGE_TUPLE = 105 20 | 21 | NIL = 106 22 | STRING = 107 23 | LIST = 108 24 | BIN = 109 25 | 26 | FUN = 117 27 | NEW_FUN = 112 28 | end 29 | 30 | VERSION = 131 31 | 32 | MAX_INT = (1 << 27) -1 33 | MIN_INT = -(1 << 27) 34 | MAX_ATOM = 255 35 | end 36 | end -------------------------------------------------------------------------------- /examples/gruff/gruff_provider.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), *%w[../../lib]) 2 | 3 | require 'erlectricity' 4 | require 'rubygems' 5 | require 'gruff' 6 | 7 | receive do |f| 8 | f.when([:plot, String, Symbol, String]) do |name, style, font| 9 | graph = Gruff.const_get(style).new 10 | graph.title = name 11 | graph.font = font 12 | graph.legend_font_size = 10 13 | 14 | f.receive do |g| 15 | g.when([:data, Symbol, Array]) do |name, points| 16 | graph.data name, points 17 | g.receive_loop 18 | end 19 | 20 | g.when([:labels, Erl.hash]) do |label_data| 21 | graph.labels = label_data 22 | g.receive_loop 23 | end 24 | 25 | g.when(:end) { :ok } 26 | end 27 | 28 | f.send!([:result, graph.to_blob]) 29 | f.receive_loop 30 | end 31 | end -------------------------------------------------------------------------------- /lib/erlectricity/conditions/static.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class StaticCondition < Condition 3 | attr_accessor :value 4 | def initialize(value) 5 | if value.is_a?(Array) 6 | self.value = value.map do |v| 7 | Condition.for(v) 8 | end 9 | else 10 | self.value = value 11 | end 12 | end 13 | 14 | def satisfies?(arg) 15 | if value.is_a?(Array) 16 | return false unless arg.is_a?(Array) 17 | return false if value.length != arg.length 18 | value.zip(arg).all? do |l, r| 19 | l.respond_to?(:satisfies?) ? l.satisfies?(r) : l.eql?(r) 20 | end 21 | else 22 | arg.eql?(value) 23 | end 24 | end 25 | 26 | def binding_for(arg) 27 | if value.is_a?(Array) 28 | value.zip(arg).map { |l, r| l.binding_for(r) }.compact 29 | else 30 | nil 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + '/../lib') 2 | 3 | require 'erlectricity' 4 | require 'rubygems' 5 | require 'test/unit' 6 | require 'test/spec' 7 | require 'stringio' 8 | 9 | $stdout.sync = true 10 | 11 | class Test::Unit::TestCase 12 | def run_erl(code) 13 | cmd = %Q{erl -noshell -eval "A = #{code.split.join(' ')}, io:put_chars(binary_to_list(A))." -s erlang halt} 14 | `#{cmd}` 15 | end 16 | 17 | def encode_packet(code) 18 | bin = run_erl("term_to_binary(#{code})") 19 | [bin.length, bin].pack("Na#{bin.length}") 20 | end 21 | 22 | def word_length 23 | (1.size * 8) - 2 24 | end 25 | end 26 | 27 | class FakePort < Erlectricity::Port 28 | attr_reader :sent 29 | attr_reader :terms 30 | 31 | def initialize(*terms) 32 | @terms = terms 33 | @sent = [] 34 | super(StringIO.new(""), StringIO.new("")) 35 | end 36 | 37 | def send(term) 38 | sent << term 39 | end 40 | 41 | private 42 | 43 | def read_from_input 44 | @terms.shift 45 | end 46 | end -------------------------------------------------------------------------------- /lib/erlectricity.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. ext]) 2 | 3 | require 'stringio' 4 | 5 | require 'erlectricity/constants' 6 | require 'erlectricity/types/new_reference' 7 | require 'erlectricity/types/pid' 8 | require 'erlectricity/types/function' 9 | require 'erlectricity/types/list' 10 | 11 | begin 12 | # try to load the decoder C extension 13 | require 'decoder' 14 | rescue LoadError 15 | # fall back on the pure ruby version 16 | require 'erlectricity/decoder' 17 | end 18 | 19 | require 'erlectricity/encoder' 20 | 21 | require 'erlectricity/port' 22 | require 'erlectricity/matcher' 23 | require 'erlectricity/condition' 24 | require 'erlectricity/conditions/boolean' 25 | require 'erlectricity/conditions/hash' 26 | require 'erlectricity/conditions/static' 27 | require 'erlectricity/conditions/type' 28 | require 'erlectricity/receiver' 29 | require 'erlectricity/errors/erlectricity_error' 30 | require 'erlectricity/errors/decode_error' 31 | require 'erlectricity/errors/encode_error' 32 | 33 | Erl = Erlectricity -------------------------------------------------------------------------------- /test/port_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper.rb' 2 | 3 | context "A port" do 4 | specify "should return terms from the queue if it is not empty" do 5 | port = FakePort.new() 6 | port.queue.clear 7 | port.queue << :foo << :bar 8 | port.receive.should == :foo 9 | port.receive.should == :bar 10 | port.receive.should == nil 11 | end 12 | 13 | specify "should read_from_input if the queue gets empty" do 14 | port = FakePort.new(:bar) 15 | port.queue.clear 16 | port.queue << :foo 17 | port.receive.should == :foo 18 | port.receive.should == :bar 19 | port.receive.should == nil 20 | end 21 | 22 | specify "should put the terms in skipped at the front of queue when restore_skipped is called" do 23 | port = FakePort.new(:baz) 24 | port.queue.clear 25 | port.queue << :bar 26 | port.skipped << :foo 27 | port.restore_skipped 28 | 29 | port.receive.should == :foo 30 | port.receive.should == :bar 31 | port.receive.should == :baz 32 | port.receive.should == nil 33 | end 34 | end -------------------------------------------------------------------------------- /lib/erlectricity/port.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class Port 3 | attr_reader :input, :output 4 | attr_reader :skipped 5 | attr_reader :queue 6 | 7 | def initialize(input=STDIN, output=STDOUT) 8 | @input = input 9 | @output = output 10 | 11 | input.sync = true 12 | output.sync = true 13 | 14 | @encoder = Erlectricity::Encoder.new(nil) 15 | @skipped = [] 16 | @queue = [] 17 | end 18 | 19 | def receive 20 | queue.empty? ? read_from_input : queue.shift 21 | end 22 | 23 | def send(term) 24 | @encoder.out = StringIO.new('', 'w') 25 | @encoder.write_any(term) 26 | data = @encoder.out.string 27 | output.write([data.length].pack("N")) 28 | output.write(data) 29 | end 30 | 31 | def restore_skipped 32 | @queue = self.skipped + self.queue 33 | end 34 | 35 | private 36 | 37 | def read_from_input 38 | raw = input.read(4) 39 | return nil unless raw 40 | 41 | packet_length = raw.unpack('N').first 42 | data = input.read(packet_length) 43 | Erlectricity::Decoder.decode(data) 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Scott Fleckenstein and Tom Preston-Werner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/simple/rerl.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | main(_) -> 3 | Cmd = "ruby rerl.rb", 4 | Port = open_port({spawn, Cmd}, [{packet, 4}, nouse_stdio, exit_status, binary]), 5 | loop(Port). 6 | 7 | send(Port, Message) -> 8 | io:format("[erlang] sending: ~p~n", [Message]), 9 | % can also use ! instead of port_command 10 | % Port ! { self(), { command, term_to_binary(Message) } }. 11 | port_command(Port, term_to_binary(Message)). 12 | 13 | loop(Port) -> 14 | receive 15 | {Port, {data, In}} -> 16 | Data = binary_to_term(In), 17 | process(Port, Data); 18 | Any -> 19 | io:format("[erlang] other: ~p~n", [Any]) 20 | end, 21 | loop(Port). 22 | 23 | process(Port, i_am_alive) -> 24 | io:format("[erlang] ruby is alive~n"), 25 | 26 | send(Port, test), 27 | send(Port, {atom, symbol}), 28 | send(Port, {bool, true}), 29 | send(Port, {number, 1}), 30 | send(Port, {string, <<"reverse">>}), 31 | send(Port, {array, [1,2,3]}), 32 | send(Port, {array, [<<"abc">>, <<"cde">>]}), 33 | send(Port, {hash, [{key,val}]}), 34 | send(Port, {object, {1,{2},3,<<"four">>}}); 35 | 36 | process(Port, Data) -> 37 | io:format("[erlang] received: ~p~n", [Data]). -------------------------------------------------------------------------------- /lib/erlectricity/condition.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class Condition 3 | def self.for(a) 4 | case a 5 | when Condition then a 6 | when Class then TypeCondition.new(a) 7 | else StaticCondition.new(a) 8 | end 9 | end 10 | 11 | def initialize 12 | end 13 | 14 | def binding_for(arg) 15 | nil 16 | end 17 | 18 | def satisfies?(arg) 19 | false 20 | end 21 | 22 | alias === satisfies? 23 | end 24 | 25 | module Conditions 26 | def atom 27 | TypeCondition.new(Symbol) 28 | end 29 | 30 | def any 31 | TypeCondition.new(Object) 32 | end 33 | 34 | def number 35 | TypeCondition.new(Fixnum) 36 | end 37 | 38 | def pid 39 | TypeCondition.new(Erlectricity::Pid) 40 | end 41 | 42 | def ref 43 | TypeCondition.new(Erlectricity::NewReference) 44 | end 45 | 46 | def string 47 | TypeCondition.new(String) 48 | end 49 | 50 | def list 51 | TypeCondition.new(Array) 52 | end 53 | 54 | def hash 55 | HashCondition.new() 56 | end 57 | 58 | def boolean 59 | BooleanCondition.new() 60 | end 61 | end 62 | 63 | extend Conditions 64 | end 65 | 66 | Any = Object -------------------------------------------------------------------------------- /examples/tinderl/tinderl.erl: -------------------------------------------------------------------------------- 1 | -module(tinderl). 2 | -export([start/4, stop/0, speak/1, paste/1]). 3 | 4 | start(Domain, Email, Password, Room) -> 5 | spawn(fun() -> 6 | register(tinderl, self()), 7 | process_flag(trap_exit, true), 8 | Cmd = lists:flatten(io_lib:format("ruby ./tinderl.rb ~s ~s ~s ~s", [Domain, Email, Password, Room])), 9 | Port = open_port({spawn, Cmd}, [{packet, 4}, nouse_stdio, exit_status, binary]), 10 | port_loop(Port) 11 | end). 12 | 13 | stop() -> tinderl ! stop. 14 | 15 | speak(String) when is_list(String) -> speak(list_to_binary(String)); 16 | speak(String) when is_binary(String) -> tinderl ! {speak, self(), String}. 17 | 18 | paste(String) when is_list(String) -> speak(list_to_binary(String)); 19 | paste(String) when is_binary(String) -> tinderl ! {paste, self(), String}. 20 | 21 | port_loop(Port) -> 22 | receive 23 | {speak, _Caller, String} -> 24 | Data = term_to_binary({speak, String}), 25 | Port ! {self(), {command, Data}}, 26 | 27 | port_loop(Port); 28 | 29 | {paste, _Caller, String} -> 30 | Data = term_to_binary({paste, String}), 31 | Port ! {self(), {command, Data}}, 32 | 33 | port_loop(Port); 34 | 35 | stop -> 36 | Port ! {self(), close}, 37 | receive 38 | {Port, closed} -> exit(normal) 39 | end; 40 | 41 | {'EXIT', Port, Reason} -> 42 | exit({port_terminated,Reason}) 43 | end. -------------------------------------------------------------------------------- /examples/gruff/stat_writer.erl: -------------------------------------------------------------------------------- 1 | -module(stat_writer). 2 | -export([start/2, loop/3]). 3 | 4 | start(Title, Fun) -> 5 | spawn(?MODULE, loop, [Title, Fun, []]). 6 | 7 | loop(Title, Fun, []) -> 8 | Data = accumulate([], Fun()), 9 | loop(Title, Fun, Data, 0). 10 | 11 | loop(Title, Fun, Data, Generation) -> 12 | receive 13 | {stop} -> ok 14 | after 3000 -> 15 | NewGeneration = Generation + 1, 16 | NewData = accumulate(Data, Fun()), 17 | NewChart = gruff:plot( 18 | list_to_binary([Title, << "- Generation" >>, integer_to_list(NewGeneration)]), 19 | <<"/Users/scott/Library/Fonts/Arial">>, 20 | NewData, 21 | [] 22 | ), 23 | file:write_file(io_lib:format("~s - ~s.png", [Title, integer_to_list(NewGeneration)]),NewChart), 24 | loop(Title, Fun, NewData, NewGeneration) 25 | end. 26 | 27 | process_axis({Name, PreviousReadings}, {Name, Reading}) -> 28 | {Name, [Reading|PreviousReadings]}. 29 | 30 | accumulate(Data, []) -> Data; 31 | accumulate([], [{Name, Reading}|Rest]) -> 32 | Data = [{Name, [Reading]}], 33 | accumulate(Data, Rest); 34 | accumulate(Data, [{Name, Reading}|Rest]) -> 35 | MergedData = case lists:keysearch(Name, 1, Data) of 36 | {value, Axis} -> lists:keyreplace(Name, 1, Data, process_axis(Axis, {Name, Reading})); 37 | false -> 38 | [{Name, [Reading]}|Data] 39 | end, 40 | accumulate(MergedData, Rest). -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 1.1.1 / 2009-10-28 2 | * Bug Fixes 3 | * Fix bignum encoding 4 | * Prevent stack overflow for massive binaries in c decoder 5 | * Optimize strings in c decoder 6 | 7 | == 1.1.0 / 2009-10-08 8 | * Minor Improvements 9 | * Implement Bignum encoding 10 | * Fix tests on Erlang R13B 11 | 12 | == 1.0.2 / 2009-06-03 13 | * Bug Fixes 14 | * Fix decoding of atoms [github.com/bwbuchanan] 15 | * Work around Ruby's reluctance to convert the empty string to 16 | a symbol [github.com/bwbuchanan] 17 | 18 | == 1.0.1 / 2009-05-03 19 | * Bug Fixes 20 | * Fix premature null byte termination on binary decode 21 | 22 | == 1.0.0 / 2009-04-29 23 | * Backward Incompatible Changes 24 | * Implicit array call for f.when(:echo, String) must now be called as 25 | f.when([:echo, String]) to eliminate ambiguity 26 | * Implicit array call for f.send!(:ok, "foo") must now be called as 27 | f.send!([:ok, "foo"]) to eliminate ambiguity 28 | * The receive loop now defaults to using file descriptors 3 and 3 29 | instead of STDIN and STDOUT. This prevents inadvertent output 30 | statements from silently corrupting the transfer protocol. 31 | * Erlang atoms 'true' and 'false' are now converted into Ruby booleans 32 | * Erlectricity::Decoder.read_any_from -> Erlectricity::Decoder.decode 33 | * Major Enhancements 34 | * Calling `rake` now runs tests (with and without compiled C extensions) 35 | * Package management is now done by Jeweler -------------------------------------------------------------------------------- /lib/erlectricity/receiver.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class Receiver 3 | attr_accessor :port 4 | attr_accessor :parent 5 | attr_accessor :matchers 6 | 7 | RECEIVE_LOOP = Object.new 8 | NO_MATCH = Object.new 9 | 10 | def initialize(port, parent = nil, &block) 11 | @port = port 12 | @parent = parent 13 | @matchers = [] 14 | block.call(self) if block 15 | end 16 | 17 | def process(arg) 18 | matcher = @matchers.find { |r| r.matches?(arg) } 19 | 20 | if matcher 21 | port.restore_skipped 22 | matcher.run(arg) 23 | else 24 | NO_MATCH 25 | end 26 | end 27 | 28 | def when(arg, &block) 29 | condition = Condition.for(arg) 30 | @matchers << Matcher.new(self, condition, block) 31 | end 32 | 33 | def run 34 | loop do 35 | msg = port.receive 36 | return if msg.nil? 37 | 38 | case result = process(msg) 39 | when RECEIVE_LOOP then next 40 | when NO_MATCH 41 | port.skipped << msg 42 | next 43 | else 44 | break result 45 | end 46 | end 47 | end 48 | 49 | def receive(&block) 50 | Receiver.new(port, self, &block).run 51 | end 52 | 53 | def receive_loop 54 | RECEIVE_LOOP 55 | end 56 | 57 | def send!(term) 58 | port.send(term) 59 | end 60 | end 61 | end 62 | 63 | module Kernel 64 | def receive(input = nil, output = nil, &block) 65 | input ||= IO.new(3) 66 | output ||= IO.new(4) 67 | Erlectricity::Receiver.new(Erlectricity::Port.new(input, output), nil, &block).run 68 | end 69 | end -------------------------------------------------------------------------------- /examples/gruff/gruff.erl: -------------------------------------------------------------------------------- 1 | -module(gruff). 2 | -export([start/0, stop/0, plot/4]). 3 | 4 | start() -> 5 | spawn(fun() -> 6 | register(gruff, self()), 7 | process_flag(trap_exit, true), 8 | Cmd = "ruby ./gruff_provider.rb", 9 | Port = open_port({spawn, Cmd}, [{packet, 4}, nouse_stdio, exit_status, binary]), 10 | port_loop(Port) 11 | end). 12 | 13 | stop() -> gruff ! stop. 14 | 15 | plot(Name, Font, Data, Labels) -> 16 | gruff ! {plot, self(), Name, Font, Data, Labels}, 17 | receive 18 | {result, Bin} -> Bin 19 | end. 20 | 21 | send_data(_Port, []) -> ok; 22 | send_data(Port, [{Name, Points}|Rest]) -> 23 | Data = {data, Name, Points}, 24 | Port ! {self(), {command, term_to_binary(Data)}}, 25 | send_data(Port, Rest). 26 | 27 | send_labels(Port, Labels) -> 28 | Data = {labels, Labels}, 29 | Port ! {self(), {command, term_to_binary(Data)}}. 30 | 31 | port_loop(Port) -> 32 | receive 33 | {plot, Caller, Name, Font, Data, Labels} -> 34 | PlotData = term_to_binary({plot, Name, 'Line', Font}), 35 | Port ! {self(), {command, PlotData}}, 36 | 37 | send_data(Port, Data), 38 | send_labels(Port, Labels), 39 | 40 | EndData = term_to_binary('end'), 41 | Port ! {self(), {command, EndData}}, 42 | Result = get_result(Port), 43 | Caller ! {result, Result }, 44 | 45 | port_loop(Port); 46 | 47 | stop -> 48 | Port ! {self(), close}, 49 | receive 50 | {Port, closed} -> exit(normal) 51 | end 52 | end. 53 | 54 | get_result(Port) -> 55 | receive 56 | {Port, {data, Data}} -> 57 | {result, Bin} = binary_to_term(Data), 58 | Bin; 59 | {'EXIT', Port, Reason} -> 60 | exit({port_terminated,Reason}) 61 | end. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "erlectricity" 8 | gem.rubyforge_project = "erlectricity" 9 | gem.summary = "A library to interface erlang and ruby through the erlang port system" 10 | gem.email = "tom@mojombo.com" 11 | gem.homepage = "http://github.com/mojombo/erlectricity" 12 | gem.authors = ["Scott Fleckenstein", "Tom Preston-Werner"] 13 | gem.require_paths = ["lib", "ext"] 14 | gem.files.include("ext") 15 | gem.extensions << 'ext/extconf.rb' 16 | 17 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 18 | end 19 | rescue LoadError 20 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 21 | end 22 | 23 | task :test do 24 | require 'open3' 25 | require 'fileutils' 26 | 27 | puts "\nCleaning extension build files and running all specs in native ruby mode..." 28 | `rm -f ext/*.bundle` && puts("rm -f ext/*.bundle") 29 | `rm -f ext/*.o` && puts("rm -f ext/*.o") 30 | Open3.popen3("ruby test/spec_suite.rb") do |stdin, stdout, stderr| 31 | while !stdout.eof? 32 | print stdout.read(1) 33 | end 34 | end 35 | 36 | puts "\nRunning `make` to build extensions and rerunning decoder specs..." 37 | Dir.chdir('ext') { `make` } 38 | Open3.popen3("ruby test/decode_spec.rb") do |stdin, stdout, stderr| 39 | while !stdout.eof? 40 | print stdout.read(1) 41 | end 42 | end 43 | end 44 | 45 | begin 46 | require 'rcov/rcovtask' 47 | Rcov::RcovTask.new do |test| 48 | test.libs << 'test' 49 | test.pattern = 'test/**/*_test.rb' 50 | test.verbose = true 51 | end 52 | rescue LoadError 53 | task :rcov do 54 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 55 | end 56 | end 57 | 58 | 59 | task :default => :test 60 | 61 | require 'rake/rdoctask' 62 | Rake::RDocTask.new do |rdoc| 63 | if File.exist?('VERSION.yml') 64 | config = YAML.load(File.read('VERSION.yml')) 65 | version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}" 66 | else 67 | version = "" 68 | end 69 | 70 | rdoc.rdoc_dir = 'rdoc' 71 | rdoc.title = "erlectricity #{version}" 72 | rdoc.rdoc_files.include('README*') 73 | rdoc.rdoc_files.include('lib/**/*.rb') 74 | end -------------------------------------------------------------------------------- /test/receiver_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper.rb' 2 | 3 | def simple_receiver_and_port(*terms, &block) 4 | port = FakePort.new(*terms) 5 | receiver = if block 6 | Erlectricity::Receiver.new(port, &block) 7 | else 8 | Erlectricity::Receiver.new(port) do |f| 9 | f.when Erl.any do 10 | :matched 11 | end 12 | end 13 | end 14 | end 15 | 16 | context "When a receiver is passed a message that matches two match blocks it" do 17 | setup do 18 | @port = FakePort.new([:foo, :foo]) 19 | @receiver = Erlectricity::Receiver.new(@port) do |f| 20 | f.when([:foo, :foo]) do 21 | :first 22 | end 23 | 24 | f.when([:foo, Erl.any]) do 25 | :second 26 | end 27 | end 28 | end 29 | 30 | specify "should run the first matching receiver's block" do 31 | @receiver.run.should == :first 32 | end 33 | end 34 | 35 | context "A receiver" do 36 | specify "should return the result of the match block when finished" do 37 | simple_receiver_and_port(:foo).run.should == :matched 38 | simple_receiver_and_port(:bar).run.should == :matched 39 | simple_receiver_and_port(:bar, :baz).run.should == :matched 40 | end 41 | 42 | specify "should process another message if the matched block returns the results of receive_loop" do 43 | recv = simple_receiver_and_port(:foo, :bar, :baz) do |f| 44 | f.when(:bar) { } 45 | f.when(Erl.any) { f.receive_loop } 46 | end 47 | 48 | recv.run 49 | recv.port.terms.should == [:baz] 50 | end 51 | 52 | specify "should properly nest" do 53 | @port = FakePort.new(:foo, :bar, :baz) 54 | @receiver = Erlectricity::Receiver.new(@port) do |f| 55 | f.when(:foo) do 56 | f.receive do |g| 57 | g.when(:bar){ :ok } 58 | end 59 | f.receive_loop 60 | end 61 | 62 | f.when(:baz) do 63 | :done 64 | end 65 | end 66 | 67 | @receiver.run.should == :done 68 | @port.terms.should == [] 69 | end 70 | 71 | specify "should queue up skipped results and restore them when a match happens" do 72 | @port = FakePort.new(:foo, :baz, :bar) 73 | @receiver = Erlectricity::Receiver.new(@port) do |f| 74 | f.when(:foo) do 75 | f.receive do |g| 76 | g.when(:bar){ :ok } 77 | end 78 | f.receive_loop 79 | end 80 | 81 | f.when(:baz) do 82 | :done 83 | end 84 | end 85 | 86 | @receiver.run.should == :done 87 | @port.terms.should == [] 88 | end 89 | 90 | specify "should expose bindings to the matched block" do 91 | @port = FakePort.new(:foo, :bar, :baz) 92 | results = [] 93 | @receiver = Erlectricity::Receiver.new(@port) do |f| 94 | f.when(Erl.atom) do |bindinated| 95 | results << bindinated 96 | f.receive_loop 97 | end 98 | end 99 | 100 | @receiver.run.should == nil 101 | results.should == [:foo, :bar, :baz] 102 | end 103 | end -------------------------------------------------------------------------------- /test/matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper.rb' 2 | 3 | def false_match(matcher, arg) 4 | matcher.matches?(arg).should == false 5 | end 6 | 7 | context "A matcher whose condition is a String (the class object" do 8 | setup do 9 | @matcher = Erlectricity::Matcher.new(nil, Erlectricity::TypeCondition.new(String), nil) 10 | end 11 | 12 | specify "should match any string" do 13 | @matcher.matches?("foo").should == true 14 | end 15 | 16 | specify "should not match symbols" do 17 | @matcher.matches?(:foo).should == false 18 | end 19 | end 20 | 21 | context "A matcher whose condition is Symbol (the class object)" do 22 | setup do 23 | @matcher = Erlectricity::Matcher.new(nil, Erlectricity::TypeCondition.new(Symbol), nil) 24 | end 25 | 26 | specify "should match any symbol" do 27 | @matcher.matches?(:foo).should == true 28 | @matcher.matches?(:bar).should == true 29 | @matcher.matches?(:baz).should == true 30 | end 31 | 32 | specify "should not match strings" do 33 | @matcher.matches?("foo").should == false 34 | @matcher.matches?("bar").should == false 35 | @matcher.matches?("baz").should == false 36 | end 37 | 38 | specify "should not match a arrays" do 39 | @matcher.matches?([:foo]).should == false 40 | @matcher.matches?([:foo, :bar]).should == false 41 | @matcher.matches?([:foo, :bar, :baz]).should == false 42 | end 43 | end 44 | 45 | context "a matcher whose condition is a symbol" do 46 | setup do 47 | @matcher = Erlectricity::Matcher.new(nil, Erlectricity::StaticCondition.new(:foo), nil) 48 | end 49 | 50 | specify "should match that symbol" do 51 | @matcher.matches?(:foo).should == true 52 | end 53 | 54 | specify "should not match any other symbol" do 55 | @matcher.matches?(:bar).should == false 56 | @matcher.matches?(:baz).should == false 57 | end 58 | end 59 | 60 | context "a matcher whose matcher is an array" do 61 | 62 | specify "should match if all of its children match" do 63 | Erlectricity::Matcher.new(nil, [Erlectricity::StaticCondition.new(:speak), Erlectricity::TypeCondition.new(Object)], nil).matches?([:paste, "haha"]).should == false 64 | 65 | matcher = Erlectricity::Matcher.new(nil, [Erlectricity::StaticCondition.new(:foo), Erlectricity::StaticCondition.new(:bar)], nil) 66 | matcher.matches?([:foo, :bar]).should == true 67 | end 68 | 69 | specify "should not match any of its children dont match" do 70 | matcher = Erlectricity::Matcher.new(nil, [Erlectricity::StaticCondition.new(:foo), Erlectricity::StaticCondition.new(:bar)], nil) 71 | matcher.matches?([:foo]).should == false 72 | matcher.matches?([:foo, :bar, :baz]).should == false 73 | matcher.matches?([:fooo, :barr]).should == false 74 | matcher.matches?([3, :bar]).should == false 75 | end 76 | 77 | specify "should not match if arg isn't an array" do 78 | matcher = Erlectricity::Matcher.new(nil, [Erlectricity::StaticCondition.new(:foo), Erlectricity::StaticCondition.new(:bar)], nil) 79 | matcher.matches?(:foo).should == false 80 | end 81 | end -------------------------------------------------------------------------------- /test/condition_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper.rb' 2 | 3 | context "Erlectricity::StaticConditions" do 4 | specify "should satisfy on the same value" do 5 | Erlectricity::StaticCondition.new(:foo).satisfies?(:foo).should == true 6 | Erlectricity::StaticCondition.new([:foo]).satisfies?([:foo]).should == true 7 | Erlectricity::StaticCondition.new(3).satisfies?(3).should == true 8 | end 9 | 10 | specify "should not satisfy on different values" do 11 | Erlectricity::StaticCondition.new(:foo).satisfies?("foo").should == false 12 | Erlectricity::StaticCondition.new([:foo]).satisfies?(:foo).should == false 13 | Erlectricity::StaticCondition.new(Object.new).satisfies?(Object.new).should == false 14 | Erlectricity::StaticCondition.new(3).satisfies?(3.0).should == false 15 | end 16 | 17 | specify "should not produce any bindings" do 18 | s = Erlectricity::StaticCondition.new(:foo) 19 | s.binding_for(:foo).should == nil 20 | end 21 | end 22 | 23 | context "Erlectricity::TypeConditions" do 24 | specify "should be satisfied when the arg has the same class" do 25 | Erlectricity::TypeCondition.new(Symbol).satisfies?(:foo).should == true 26 | Erlectricity::TypeCondition.new(Symbol).satisfies?(:bar).should == true 27 | Erlectricity::TypeCondition.new(String).satisfies?("foo").should == true 28 | Erlectricity::TypeCondition.new(String).satisfies?("bar").should == true 29 | Erlectricity::TypeCondition.new(Array).satisfies?([]).should == true 30 | Erlectricity::TypeCondition.new(Fixnum).satisfies?(3).should == true 31 | end 32 | 33 | specify "should be satisfied when the arg is of a descendent class" do 34 | Erlectricity::TypeCondition.new(Object).satisfies?(:foo).should == true 35 | Erlectricity::TypeCondition.new(Object).satisfies?("foo").should == true 36 | Erlectricity::TypeCondition.new(Object).satisfies?(3).should == true 37 | end 38 | 39 | specify "should not be satisfied when the arg is of a different class" do 40 | Erlectricity::TypeCondition.new(String).satisfies?(:foo).should == false 41 | Erlectricity::TypeCondition.new(Symbol).satisfies?("foo").should == false 42 | Erlectricity::TypeCondition.new(Fixnum).satisfies?(3.0).should == false 43 | end 44 | 45 | specify "should bind the arg with no transormations" do 46 | s = Erlectricity::TypeCondition.new(Symbol) 47 | s.binding_for(:foo).should == :foo 48 | s.binding_for(:bar).should == :bar 49 | end 50 | end 51 | 52 | context "Erlectricity::HashConditions" do 53 | specify "should satisfy an args of the form [[key, value], [key, value]]" do 54 | Erlectricity::HashCondition.new.satisfies?([[:foo, 3], [:bar, Object.new]]).should == true 55 | Erlectricity::HashCondition.new.satisfies?([[:foo, 3]]).should == true 56 | end 57 | 58 | specify "should satisfy on empty arrays" do 59 | Erlectricity::HashCondition.new.satisfies?([]).should == true 60 | end 61 | 62 | specify "should nat satisfy other args" do 63 | Erlectricity::HashCondition.new.satisfies?(:foo).should == false 64 | Erlectricity::HashCondition.new.satisfies?("foo").should == false 65 | Erlectricity::HashCondition.new.satisfies?(3.0).should == false 66 | end 67 | 68 | specify "should bind to a Hash" do 69 | s = Erlectricity::HashCondition.new() 70 | s.binding_for([[:foo, 3], [:bar, [3,4,5]]]).should == {:foo => 3, :bar => [3,4,5] } 71 | end 72 | end -------------------------------------------------------------------------------- /examples/simple/rerl.rb: -------------------------------------------------------------------------------- 1 | # 2 | # rerl.rb, for use with erlang's open_port 3 | # spawn using rerl.sh escript: 4 | # 5 | # $ ./rerl.sh 6 | # ./rerl.sh:35: Warning: variable 'Port' is unused 7 | # [erlang] ruby is alive 8 | # [erlang] sending: test 9 | # [erlang] sending: {atom,symbol} 10 | # [erlang] sending: {number,1} 11 | # [erlang] sending: {string,<<"reverse">>} 12 | # [erlang] sending: {array,[1,2,3]} 13 | # [erlang] sending: {array,[<<"abc">>,<<"cde">>]} 14 | # [erlang] sending: {hash,[{key,val}]} 15 | # [erlang] sending: {object,{1,{2},3,<<"four">>}} 16 | # [ ruby ] received: test, nil 17 | # [ ruby ] sending: test, nil 18 | # [erlang] received: test 19 | # [ ruby ] received: atom, :symbol 20 | # [ ruby ] sending: atom, :lobmys 21 | # [erlang] received: {atom,lobmys} 22 | # [ ruby ] received: number, 1 23 | # [ ruby ] sending: number, 2 24 | # [ ruby ] received: string, "reverse" 25 | # [erlang] received: {number,2} 26 | # [ ruby ] sending: string, "esrever" 27 | # [ ruby ] received: array, [1, 2, 3] 28 | # [erlang] received: {string,<<"esrever">>} 29 | # [ ruby ] sending: array, [3, 2, 1] 30 | # [ ruby ] received: array, ["abc", "cde"] 31 | # [erlang] received: {array,{3,2,1}} 32 | # [ ruby ] sending: array, ["cde", "abc"] 33 | # [ ruby ] received: hash, {:key=>:val} 34 | # [erlang] received: {array,{<<"cde">>,<<"abc">>}} 35 | # [ ruby ] sending: hash, {:key=>:val, :ruby=>:true} 36 | # [ ruby ] received: object, [1, [2], 3, "four"] 37 | # [erlang] received: {hash,{{key,val},{ruby,true}}} 38 | # [ ruby ] sending: object, [1, [2], 3, "four"] 39 | # [erlang] received: {object,{1,{2},3,<<"four">>}} 40 | # 41 | 42 | $:.unshift File.join(File.dirname(__FILE__), *%w[../../lib]) 43 | 44 | require 'rubygems' 45 | require 'erlectricity' 46 | 47 | def log arg 48 | puts arg 49 | # @f ||= File.open('/tmp/rerl.log', 'w') 50 | # @f.puts arg 51 | # @f.flush 52 | end 53 | 54 | def debug meth, got = nil, send = nil 55 | log "[ ruby ] received: #{meth}, #{got.inspect}" 56 | log "[ ruby ] sending: #{meth}, #{send.inspect}" 57 | end 58 | 59 | receive do |f| 60 | f.when(:test) do 61 | debug(:test) 62 | f.send!(:test) 63 | f.receive_loop 64 | end 65 | 66 | f.when([:atom, Symbol]) do |sym| 67 | debug(:atom, sym, sym.to_s.reverse.to_sym) 68 | f.send!([:atom, sym.to_s.reverse.to_sym]) 69 | f.receive_loop 70 | end 71 | 72 | f.when([:bool, Erl.boolean]) do |bool| 73 | debug(:bool, bool, !bool) 74 | f.send!([:bool, !bool]) 75 | f.receive_loop 76 | end 77 | 78 | f.when([:number, Fixnum]) do |num| 79 | debug(:number, num, num*2) 80 | f.send!([:number, num*2]) 81 | f.receive_loop 82 | end 83 | 84 | f.when([:string, String]) do |str| 85 | debug(:string, str, str.reverse) 86 | f.send!([:string, str.reverse]) 87 | f.receive_loop 88 | end 89 | 90 | f.when([:array, Array]) do |arr| 91 | debug(:array, arr, arr.reverse) 92 | f.send!([:array, arr.reverse]) 93 | f.receive_loop 94 | end 95 | 96 | f.when([:hash, Erl.hash]) do |hash| 97 | newhash = hash.dup 98 | newhash[:ruby] = :true 99 | debug(:hash, hash, newhash) 100 | f.send!([:hash, newhash.to_a]) 101 | f.receive_loop 102 | end 103 | 104 | f.when([:object, Any]) do |obj| 105 | debug(:object, obj, obj) 106 | f.send!([:object, obj]) 107 | f.receive_loop 108 | end 109 | 110 | f.send!(:i_am_alive) 111 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Erlectricity 2 | ============ 3 | 4 | http://github.com/mojombo/erlectricity 5 | 6 | By Scott Fleckenstein, Tom Preston-Werner 7 | 8 | Development Status: Production/Stable 9 | 10 | 11 | Description 12 | ----------- 13 | 14 | Erlectricity allows a Ruby program to receive and respond to Erlang messages 15 | sent over the Erlang binary protocol. 16 | 17 | 18 | Install 19 | ------- 20 | 21 | $ gem install erlectricity 22 | 23 | -or- 24 | 25 | $ gem install mojombo-erlectricity -s http://gems.github.com 26 | 27 | 28 | The Simplest Example 29 | -------------------- 30 | 31 | ### Ruby side (echo.rb) 32 | 33 | require 'rubygems' 34 | require 'erlectricity' 35 | 36 | receive do |f| 37 | f.when([:echo, String]) do |text| 38 | f.send!([:result, "You said: #{text}"]) 39 | f.receive_loop 40 | end 41 | end 42 | 43 | ### Erlang side (echo.erl) 44 | 45 | -module(echo). 46 | -export([test/0]). 47 | 48 | test() -> 49 | Cmd = "ruby echo.rb", 50 | Port = open_port({spawn, Cmd}, [{packet, 4}, nouse_stdio, exit_status, binary]), 51 | Payload = term_to_binary({echo, <<"hello world!">>}), 52 | port_command(Port, Payload), 53 | receive 54 | {Port, {data, Data}} -> 55 | {result, Text} = binary_to_term(Data), 56 | io:format("~p~n", [Text]) 57 | end. 58 | 59 | 60 | Data Type Conversions and Matching 61 | ---------------------------------- 62 | 63 | % Port is the port opened via open_port({spawn, Cmd}, [{packet, 4}, ...]) 64 | % Message is the Erlang term to encode and send to the port 65 | send(Port, Message) -> 66 | port_command(Port, term_to_binary(Message)). 67 | 68 | # Each triplet below represents: 69 | # (line 1) the Erlang call 70 | # (line 2) the Ruby matcher 71 | # (line 3) the Ruby output 72 | 73 | send(Port, test). 74 | f.when(:test) { p :ok } 75 | # :ok 76 | 77 | send(Port, {atom, symbol}). 78 | f.when([:atom, Symbol]) { |sym| p sym } 79 | # :symbol 80 | 81 | send(Port, {number, 1}). 82 | f.when([:number, Fixnum]) { |num| p num } 83 | # 1 84 | 85 | send(Port, {string, <<"foo">>}). 86 | f.when([:string, String]) { |str| p str } 87 | # "foo" 88 | 89 | send(Port, {array, [1,2,3]}). 90 | f.when([:array, Array]) { |arr| p arr } 91 | # [1, 2, 3] 92 | 93 | send(Port, {array, [<<"abc">>, <<"def">>]}). 94 | f.when([:array, Array]) { |arr| p arr } 95 | # ["abc", "def"] 96 | 97 | send(Port, {hash, [{key,val}]}). 98 | f.when([:hash, Erl.hash]) { |hash| p hash } 99 | # {:key=>:val} 100 | 101 | send(Port, {object, {1,{2},3,<<"four">>}}). 102 | f.when([:object, Any]) { |any| p any } 103 | # [1, [2], 3, "four"] 104 | 105 | 106 | Contribute 107 | ---------- 108 | 109 | If you'd like to hack on Erlectricity, start by forking my repo on GitHub: 110 | 111 | http://github.com/mojombo/erlectricity 112 | 113 | To get all of the dependencies, install the gem first. The best way to get 114 | your changes merged back into core is as follows: 115 | 116 | 1. Clone down your fork 117 | 1. Create a topic branch to contain your change 118 | 1. Hack away 119 | 1. Add tests and make sure everything still passes by running `rake` 120 | 1. If you are adding new functionality, document it in the README.md 121 | 1. Do not change the version number, I will do that on my end 122 | 1. If necessary, rebase your commits into logical chunks, without errors 123 | 1. Push the branch up to GitHub 124 | 1. Send me (mojombo) a pull request for your branch 125 | 126 | 127 | Copyright 128 | --------- 129 | 130 | Copyright (c) 2009 Scott Fleckenstein and Tom Preston-Werner. See LICENSE for details. -------------------------------------------------------------------------------- /erlectricity.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec` 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{erlectricity} 8 | s.version = "1.1.1" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Scott Fleckenstein", "Tom Preston-Werner"] 12 | s.date = %q{2009-10-28} 13 | s.email = %q{tom@mojombo.com} 14 | s.extensions = ["ext/extconf.rb"] 15 | s.extra_rdoc_files = [ 16 | "LICENSE", 17 | "README.md" 18 | ] 19 | s.files = [ 20 | ".gitignore", 21 | "History.txt", 22 | "LICENSE", 23 | "README.md", 24 | "Rakefile", 25 | "VERSION.yml", 26 | "benchmarks/bench.rb", 27 | "erlectricity.gemspec", 28 | "examples/echo/README.md", 29 | "examples/echo/echo.erl", 30 | "examples/echo/echo.rb", 31 | "examples/gruff/gruff.erl", 32 | "examples/gruff/gruff_provider.rb", 33 | "examples/gruff/gruff_run.sh", 34 | "examples/gruff/stat_run.sh", 35 | "examples/gruff/stat_writer.erl", 36 | "examples/simple/README.md", 37 | "examples/simple/rerl.rb", 38 | "examples/simple/rerl.sh", 39 | "examples/tinderl/README.md", 40 | "examples/tinderl/tinderl.erl", 41 | "examples/tinderl/tinderl.rb", 42 | "ext/decoder.c", 43 | "ext/extconf.rb", 44 | "lib/erlectricity.rb", 45 | "lib/erlectricity/condition.rb", 46 | "lib/erlectricity/conditions/boolean.rb", 47 | "lib/erlectricity/conditions/hash.rb", 48 | "lib/erlectricity/conditions/static.rb", 49 | "lib/erlectricity/conditions/type.rb", 50 | "lib/erlectricity/constants.rb", 51 | "lib/erlectricity/decoder.rb", 52 | "lib/erlectricity/encoder.rb", 53 | "lib/erlectricity/errors/decode_error.rb", 54 | "lib/erlectricity/errors/encode_error.rb", 55 | "lib/erlectricity/errors/erlectricity_error.rb", 56 | "lib/erlectricity/matcher.rb", 57 | "lib/erlectricity/port.rb", 58 | "lib/erlectricity/receiver.rb", 59 | "lib/erlectricity/types/function.rb", 60 | "lib/erlectricity/types/list.rb", 61 | "lib/erlectricity/types/new_function.rb", 62 | "lib/erlectricity/types/new_reference.rb", 63 | "lib/erlectricity/types/pid.rb", 64 | "lib/erlectricity/types/reference.rb", 65 | "lib/erlectricity/version.rb", 66 | "test/condition_spec.rb", 67 | "test/decode_spec.rb", 68 | "test/encode_spec.rb", 69 | "test/matcher_spec.rb", 70 | "test/port_spec.rb", 71 | "test/receiver_spec.rb", 72 | "test/spec_suite.rb", 73 | "test/test_helper.rb" 74 | ] 75 | s.homepage = %q{http://github.com/mojombo/erlectricity} 76 | s.rdoc_options = ["--charset=UTF-8"] 77 | s.require_paths = ["lib", "ext"] 78 | s.rubyforge_project = %q{erlectricity} 79 | s.rubygems_version = %q{1.3.5} 80 | s.summary = %q{A library to interface erlang and ruby through the erlang port system} 81 | s.test_files = [ 82 | "test/condition_spec.rb", 83 | "test/decode_spec.rb", 84 | "test/encode_spec.rb", 85 | "test/matcher_spec.rb", 86 | "test/port_spec.rb", 87 | "test/receiver_spec.rb", 88 | "test/spec_suite.rb", 89 | "test/test_helper.rb", 90 | "examples/echo/echo.rb", 91 | "examples/gruff/gruff_provider.rb", 92 | "examples/simple/rerl.rb", 93 | "examples/tinderl/tinderl.rb" 94 | ] 95 | 96 | if s.respond_to? :specification_version then 97 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 98 | s.specification_version = 3 99 | 100 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 101 | else 102 | end 103 | else 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/erlectricity/encoder.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class Encoder 3 | include Erlectricity::External::Types 4 | 5 | attr_accessor :out 6 | 7 | def initialize(out) 8 | self.out = out 9 | end 10 | 11 | def self.encode(data) 12 | io = StringIO.new 13 | self.new(io).write_any(data) 14 | io.string 15 | end 16 | 17 | def write_any obj 18 | write_1 Erlectricity::External::VERSION 19 | write_any_raw obj 20 | end 21 | 22 | def write_any_raw obj 23 | case obj 24 | when Symbol then write_symbol(obj) 25 | when Fixnum, Bignum then write_fixnum(obj) 26 | when Float then write_float(obj) 27 | when Erlectricity::NewReference then write_new_reference(obj) 28 | when Erlectricity::Pid then write_pid(obj) 29 | when Erlectricity::List then write_list(obj) 30 | when Array then write_tuple(obj) 31 | when String then write_binary(obj) 32 | when Time then write_any_raw(obj.to_i.divmod(1000000) + [obj.usec]) 33 | when TrueClass, FalseClass then write_boolean(obj) 34 | else 35 | fail(obj) 36 | end 37 | end 38 | 39 | def write_1(byte) 40 | out.write([byte].pack("C")) 41 | end 42 | 43 | def write_2(short) 44 | out.write([short].pack("n")) 45 | end 46 | 47 | def write_4(long) 48 | out.write([long].pack("N")) 49 | end 50 | 51 | def write_string(string) 52 | out.write(string) 53 | end 54 | 55 | def write_boolean(bool) 56 | write_symbol(bool.to_s.to_sym) 57 | end 58 | 59 | def write_symbol(sym) 60 | fail(sym) unless sym.is_a?(Symbol) 61 | data = sym.to_s 62 | write_1 ATOM 63 | write_2 data.length 64 | write_string data 65 | end 66 | 67 | def write_fixnum(num) 68 | if num >= 0 && num < 256 69 | write_1 SMALL_INT 70 | write_1 num 71 | elsif num <= Erlectricity::External::MAX_INT && num >= Erlectricity::External::MIN_INT 72 | write_1 INT 73 | write_4 num 74 | else 75 | write_bignum num 76 | end 77 | end 78 | 79 | def write_float(float) 80 | write_1 FLOAT 81 | write_string format("%15.15e", float).ljust(31, "\000") 82 | end 83 | 84 | def write_bignum(num) 85 | if num.is_a?(Bignum) 86 | n = num.size 87 | else 88 | n = (num.to_s(2).size / 8.0).ceil 89 | end 90 | if n <= 256 91 | write_1 SMALL_BIGNUM 92 | write_1 n 93 | write_bignum_guts(num) 94 | else 95 | write_1 LARGE_BIGNUM 96 | write_4 n 97 | write_bignum_guts(num) 98 | end 99 | end 100 | 101 | def write_bignum_guts(num) 102 | write_1 (num >= 0 ? 0 : 1) 103 | num = num.abs 104 | while num != 0 105 | rem = num % 256 106 | write_1 rem 107 | num = num >> 8 108 | end 109 | end 110 | 111 | def write_new_reference(ref) 112 | fail(ref) unless ref.is_a?(Erlectricity::NewReference) 113 | write_1 NEW_REF 114 | write_2 ref.id.length 115 | write_symbol(ref.node) 116 | write_1 ref.creation 117 | write_string ref.id.pack('N' * ref.id.length) 118 | end 119 | 120 | def write_pid(pid) 121 | fail(pid) unless pid.is_a? Erlectricity::Pid 122 | write_1 PID 123 | write_symbol(pid.node) 124 | write_4 pid.id 125 | write_4 pid.serial 126 | write_1 pid.creation 127 | end 128 | 129 | def write_tuple(data) 130 | fail(data) unless data.is_a? Array 131 | 132 | if data.length < 256 133 | write_1 SMALL_TUPLE 134 | write_1 data.length 135 | else 136 | write_1 LARGE_TUPLE 137 | write_4 data.length 138 | end 139 | 140 | data.each { |e| write_any_raw e } 141 | end 142 | 143 | def write_list(data) 144 | fail(data) unless data.is_a? Array 145 | write_1 NIL and return if data.empty? 146 | write_1 LIST 147 | write_4 data.length 148 | data.each{|e| write_any_raw e } 149 | write_1 NIL 150 | end 151 | 152 | def write_binary(data) 153 | write_1 BIN 154 | write_4 data.length 155 | write_string data 156 | end 157 | 158 | private 159 | 160 | def fail(obj) 161 | raise EncodeError, "Cannot encode to erlang external format: #{obj.inspect}" 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/decode_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper.rb' 2 | 3 | context "When unpacking from a binary stream" do 4 | setup do 5 | end 6 | 7 | specify "an erlang atom should decode to a ruby symbol" do 8 | get("haha").should == :haha 9 | end 10 | 11 | specify "an erlang number encoded as a small_int (< 255) should decode to a fixnum" do 12 | get("0").should == 0 13 | get("255").should == 255 14 | end 15 | 16 | specify "an erlang number encoded as a int (signed 27-bit number) should decode to a fixnum" do 17 | get("256").should == 256 18 | get("#{(1 << 27) -1}").should == (1 << 27) -1 19 | get("-1").should == -1 20 | get("#{-(1 << 27)}").should == -(1 << 27) 21 | end 22 | 23 | specify "an erlang number encoded as a small bignum (1 byte length) should decode to fixnum if it can" do 24 | get("#{(1 << 27)}").should == (1 << 27) 25 | get("#{-(1 << 27) - 1}").should == -(1 << 27) - 1 26 | get("#{(1 << word_length) - 1}").should == (1 << word_length) - 1 27 | get("#{-(1 << word_length)}").should == -(1 << word_length) 28 | end 29 | 30 | specify "an erlang number encoded as a small bignum (1 byte length) should decode to bignum if it can't be a fixnum" do 31 | get("#{(1 << word_length)}").should == (1 << word_length) 32 | get("#{-(1 << word_length) - 1}").should == -(1 << word_length) - 1 33 | get("#{(1 << (255 * 8)) - 1}").should == (1 << (255 * 8)) - 1 34 | get("#{-((1 << (255 * 8)) - 1)}").should == -((1 << (255 * 8)) - 1) 35 | end 36 | 37 | specify "an erlang number encoded as a big bignum (4 byte length) should decode to bignum" do 38 | get("#{(1 << (255 * 8)) }").should == (1 << (255 * 8)) 39 | get("#{-(1 << (255 * 8))}").should == -(1 << (255 * 8)) 40 | get("#{(1 << (512 * 8)) }").should == (1 << (512 * 8)) 41 | get("#{-(1 << (512 * 8))}").should == -(1 << (512 * 8)) 42 | end 43 | 44 | specify "an erlang float should decode to a Float" do 45 | get("#{1.0}").should == 1.0 46 | get("#{-1.0}").should == -1.0 47 | get("#{123.456}").should == 123.456 48 | get("#{123.456789012345}").should == 123.456789012345 49 | end 50 | 51 | specify "an erlang reference should decode to a Reference object" do 52 | ref = get("make_ref()") 53 | ref.should.be.instance_of Erlectricity::NewReference 54 | ref.node.should.be.instance_of Symbol 55 | end 56 | 57 | specify "an erlang pid should decode to a Pid object" do 58 | pid = get("spawn(fun() -> 3 end)") 59 | pid.should.be.instance_of Erlectricity::Pid 60 | pid.node.should.be.instance_of Symbol 61 | end 62 | 63 | specify "an erlang tuple encoded as a small tuple (1-byte length) should decode to an array" do 64 | ref = get("{3}") 65 | ref.length.should == 1 66 | ref.first.should == 3 67 | 68 | ref = get("{3, a, make_ref()}") 69 | ref.length.should == 3 70 | ref[0].should == 3 71 | ref[1].should == :a 72 | ref[2].class.should == Erlectricity::NewReference 73 | 74 | tuple_meat = (['3'] * 255).join(', ') 75 | ref = get("{#{tuple_meat}}") 76 | ref.length.should == 255 77 | ref.each{|r| r.should == 3} 78 | end 79 | 80 | specify "an erlang tuple encoded as a large tuple (4-byte length) should decode to an array" do 81 | tuple_meat = (['3'] * 256).join(', ') 82 | ref = get("{#{tuple_meat}}") 83 | ref.length.should == 256 84 | ref.each{|r| r.should == 3} 85 | 86 | tuple_meat = (['3'] * 512).join(', ') 87 | ref = get("{#{tuple_meat}}") 88 | ref.length.should == 512 89 | ref.each{|r| r.should == 3} 90 | end 91 | 92 | specify "an empty erlang list encoded as a nil should decode to an array" do 93 | get("[]").class.should == Erl::List 94 | get("[]").should == [] 95 | end 96 | 97 | specify "an erlang list encoded as a string should decode to an array of bytes (less than ideal, but consistent)" do 98 | get("\"asdasd\"").class.should == Erl::List 99 | get("\"asdasd\"").should == "asdasd".split('').map{|c| c[0]} 100 | get("\"#{'a' * 65534}\"").should == ['a'[0]] * 65534 101 | end 102 | 103 | specify "an erlang list encoded as a list should decode to an erl::list" do 104 | get("[3,4,256]").class.should == Erl::List 105 | get("[3,4,256]").should == [3,4,256] 106 | get("\"#{'a' * 65535 }\"").should == [97] * 65535 107 | get("[3,4, foo, {3,4,5,bar}, 256]").should == [3,4, :foo, [3,4,5,:bar], 256] 108 | end 109 | 110 | specify "an erlang binary should decode to a string" do 111 | get("<< 3,4,255 >>").should == "\003\004\377" 112 | get("<< \"whatup\" >>").should == "whatup" 113 | get("<< 99,0,99 >>").should == "c\000c" 114 | end 115 | 116 | specify "the empty atom should decode to the empty symbol" do 117 | empty_symbol = get("''") 118 | empty_symbol.should.be.instance_of Symbol 119 | empty_symbol.to_s.should == "" 120 | end 121 | 122 | specify "erlang atomic booleans should decode to ruby booleans" do 123 | get("true").should == true 124 | get("false").should == false 125 | get("falsereio").should == :falsereio 126 | get("t").should == :t 127 | get("f").should == :f 128 | end 129 | 130 | specify "massive binaries should not overflow the stack" do 131 | bin = [131,109,0,128,0,0].pack('c*') + ('a' * (8 * 1024 * 1024)) 132 | assert_equal (8 * 1024 * 1024), Erlectricity::Decoder.decode(bin).size 133 | end 134 | 135 | specify "a good thing should be awesome" do 136 | get(%Q-[{options,{struct,[{test,<<"I'm chargin' mah lazer">>}]}},{passage,<<"Why doesn't this work?">>}]-).should == 137 | [[:options, [:struct, [[:test, "I'm chargin' mah lazer"]]]], [:passage, "Why doesn't this work?"]] 138 | end 139 | 140 | def get(str) 141 | x = "term_to_binary(#{str.gsub(/"/, '\\\"')})" 142 | bin = run_erl(x) 143 | Erlectricity::Decoder.decode(bin) 144 | end 145 | end -------------------------------------------------------------------------------- /lib/erlectricity/decoder.rb: -------------------------------------------------------------------------------- 1 | module Erlectricity 2 | class Decoder 3 | attr_accessor :in 4 | include Erlectricity::External::Types 5 | 6 | def self.decode(string) 7 | new(StringIO.new(string)).read_any 8 | end 9 | 10 | def initialize(ins) 11 | @in = ins 12 | @peeked = "" 13 | end 14 | 15 | def read_any 16 | fail("Bad Magic") unless read_1 == Erlectricity::External::VERSION 17 | read_any_raw 18 | end 19 | 20 | def read_any_raw 21 | case peek_1 22 | when ATOM then read_atom 23 | when SMALL_INT then read_small_int 24 | when INT then read_int 25 | when SMALL_BIGNUM then read_small_bignum 26 | when LARGE_BIGNUM then read_large_bignum 27 | when FLOAT then read_float 28 | when NEW_REF then read_new_reference 29 | when PID then read_pid 30 | when SMALL_TUPLE then read_small_tuple 31 | when LARGE_TUPLE then read_large_tuple 32 | when NIL then read_nil 33 | when STRING then read_erl_string 34 | when LIST then read_list 35 | when BIN then read_bin 36 | else 37 | fail("Unknown term tag: #{peek_1}") 38 | end 39 | end 40 | 41 | def read(length) 42 | if length < @peeked.length 43 | result = @peeked[0...length] 44 | @peeked = @peeked[length..-1] 45 | length = 0 46 | else 47 | result = @peeked 48 | @peeked = '' 49 | length -= result.length 50 | end 51 | 52 | if length > 0 53 | result << @in.read(length) 54 | end 55 | result 56 | end 57 | 58 | def peek(length) 59 | if length <= @peeked.length 60 | @peeked[0...length] 61 | else 62 | read_bytes = @in.read(length - @peeked.length) 63 | @peeked << read_bytes if read_bytes 64 | @peeked 65 | end 66 | end 67 | 68 | def peek_1 69 | peek(1).unpack("C").first 70 | end 71 | 72 | def peek_2 73 | peek(2).unpack("n").first 74 | end 75 | 76 | def read_1 77 | read(1).unpack("C").first 78 | end 79 | 80 | def read_2 81 | read(2).unpack("n").first 82 | end 83 | 84 | def read_4 85 | read(4).unpack("N").first 86 | end 87 | 88 | def read_string(length) 89 | read(length) 90 | end 91 | 92 | def read_atom 93 | fail("Invalid Type, not an atom") unless read_1 == ATOM 94 | length = read_2 95 | a = read_string(length) 96 | case a 97 | when "true" 98 | true 99 | when "false" 100 | false 101 | when "" 102 | Marshal.load("\004\b:\005") # Workaround for inability to do ''.to_sym 103 | else 104 | a.to_sym 105 | end 106 | end 107 | 108 | def read_small_int 109 | fail("Invalid Type, not a small int") unless read_1 == SMALL_INT 110 | read_1 111 | end 112 | 113 | def read_int 114 | fail("Invalid Type, not an int") unless read_1 == INT 115 | value = read_4 116 | negative = (value >> 31)[0] == 1 117 | value = (value - (1 << 32)) if negative 118 | value = Fixnum.induced_from(value) 119 | end 120 | 121 | def read_small_bignum 122 | fail("Invalid Type, not a small bignum") unless read_1 == SMALL_BIGNUM 123 | size = read_1 124 | sign = read_1 125 | bytes = read_string(size).unpack("C" * size) 126 | added = bytes.zip((0..bytes.length).to_a).inject(0) do |result, byte_index| 127 | byte, index = *byte_index 128 | value = (byte * (256 ** index)) 129 | sign != 0 ? (result - value) : (result + value) 130 | end 131 | Bignum.induced_from(added) 132 | end 133 | 134 | def read_large_bignum 135 | fail("Invalid Type, not a large bignum") unless read_1 == LARGE_BIGNUM 136 | size = read_4 137 | sign = read_1 138 | bytes = read_string(size).unpack("C" * size) 139 | added = bytes.zip((0..bytes.length).to_a).inject(0) do |result, byte_index| 140 | byte, index = *byte_index 141 | value = (byte * (256 ** index)) 142 | sign != 0 ? (result - value) : (result + value) 143 | end 144 | Bignum.induced_from(added) 145 | end 146 | 147 | def read_float 148 | fail("Invalid Type, not a float") unless read_1 == FLOAT 149 | string_value = read_string(31) 150 | result = string_value.to_f 151 | end 152 | 153 | def read_new_reference 154 | fail("Invalid Type, not a new-style reference") unless read_1 == NEW_REF 155 | size = read_2 156 | node = read_atom 157 | creation = read_1 158 | id = (0...size).map { |i| read_4 } 159 | NewReference.new(node, creation, id) 160 | end 161 | 162 | def read_pid 163 | fail("Invalid Type, not a pid") unless read_1 == PID 164 | node = read_atom 165 | id = read_4 166 | serial = read_4 167 | creation = read_1 168 | Pid.new(node, id, serial, creation) 169 | end 170 | 171 | def read_small_tuple 172 | fail("Invalid Type, not a small tuple") unless read_1 == SMALL_TUPLE 173 | arity = read_1 174 | (0...arity).map { |i| read_any_raw } 175 | end 176 | 177 | def read_large_tuple 178 | fail("Invalid Type, not a small tuple") unless read_1 == LARGE_TUPLE 179 | arity = read_4 180 | (0...arity).map { |i| read_any_raw } 181 | end 182 | 183 | def read_nil 184 | fail("Invalid Type, not a nil list") unless read_1 == NIL 185 | Erlectricity::List.new([]) 186 | end 187 | 188 | def read_erl_string 189 | fail("Invalid Type, not an erlang string") unless read_1 == STRING 190 | length = read_2 191 | Erlectricity::List.new(read_string(length).unpack('C' * length)) 192 | end 193 | 194 | def read_list 195 | fail("Invalid Type, not an erlang list") unless read_1 == LIST 196 | length = read_4 197 | list = (0...length).map { |i| read_any_raw } 198 | read_1 199 | Erlectricity::List.new(list) 200 | end 201 | 202 | def read_bin 203 | fail("Invalid Type, not an erlang binary") unless read_1 == BIN 204 | length = read_4 205 | read_string(length) 206 | end 207 | 208 | def fail(str) 209 | raise DecodeError, str 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /test/encode_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper.rb' 2 | 3 | context "When packing to a binary stream" do 4 | setup do 5 | @out = StringIO.new('', 'w') 6 | @encoder = Erlectricity::Encoder.new(@out) 7 | end 8 | 9 | specify "A symbol should be encoded to an erlang atom" do 10 | get{@encoder.write_symbol :haha}.should == get_erl("haha") 11 | write_any(:haha).should == get_erl_with_magic("haha") 12 | end 13 | 14 | specify "A boolean should be encoded to an erlang atom" do 15 | get{@encoder.write_boolean true}.should == get_erl("true") 16 | get{@encoder.write_boolean false}.should == get_erl("false") 17 | write_any(true).should == get_erl_with_magic("true") 18 | write_any(false).should == get_erl_with_magic("false") 19 | end 20 | 21 | specify "A number should be encoded as an erlang number would be" do 22 | #SMALL_INTS 23 | get{@encoder.write_fixnum 0}.should == get_erl("0") 24 | get{@encoder.write_fixnum 255}.should == get_erl("255") 25 | write_any(0).should == get_erl_with_magic("0") 26 | write_any(255).should == get_erl_with_magic("255") 27 | 28 | #INTS 29 | get{@encoder.write_fixnum 256}.should == get_erl("256") 30 | get{@encoder.write_fixnum((1 << 27) - 1)}.should == get_erl("#{(1 << 27) - 1}") 31 | get{@encoder.write_fixnum(-1)}.should == get_erl("-1") 32 | get{@encoder.write_fixnum(-(1 << 27))}.should == get_erl("#{-(1 << 27)}") 33 | write_any(256).should == get_erl_with_magic("256") 34 | write_any((1 << 27) - 1).should == get_erl_with_magic("#{(1 << 27) - 1}") 35 | write_any(-1).should == get_erl_with_magic("-1") 36 | write_any(-(1 << 27)).should == get_erl_with_magic("#{-(1 << 27)}") 37 | 38 | # #SMALL_BIGNUMS 39 | get{@encoder.write_fixnum(10_000_000_000_000_000_000)}.should == get_erl("10000000000000000000") 40 | get{@encoder.write_fixnum(1254976067)}.should == get_erl("1254976067") 41 | get{@encoder.write_fixnum(-1254976067)}.should == get_erl("-1254976067") 42 | # get{@encoder.write_fixnum((1 << word_length))}.should == get_erl("#{(1 << word_length)}") 43 | # get{@encoder.write_fixnum(-(1 << word_length) - 1)}.should == get_erl("#{-(1 << word_length) - 1}") 44 | # get{@encoder.write_fixnum((1 << (255 * 8)) - 1)}.should == get_erl("#{(1 << (255 * 8)) - 1}") 45 | # get{@encoder.write_fixnum(-((1 << (255 * 8)) - 1))}.should == get_erl("#{-((1 << (255 * 8)) - 1)}") 46 | # 47 | # write_any((1 << word_length)).should == get_erl_with_magic("#{(1 << word_length)}") 48 | # write_any(-(1 << word_length) - 1).should == get_erl_with_magic("#{-(1 << word_length) - 1}") 49 | # write_any((1 << (255 * 8)) - 1).should == get_erl_with_magic("#{(1 << (255 * 8)) - 1}") 50 | # write_any(-((1 << (255 * 8)) - 1)).should == get_erl_with_magic("#{-((1 << (255 * 8)) - 1)}") 51 | # 52 | # #LARGE_BIGNUMS 53 | x = 1254976067 ** 256 54 | get{@encoder.write_fixnum(x)}.should == get_erl("#{x}") 55 | get{@encoder.write_fixnum(-x)}.should == get_erl("-#{x}") 56 | # get{@encoder.write_fixnum((1 << (255 * 8)))}.should == get_erl("#{(1 << (255 * 8))}") 57 | # get{@encoder.write_fixnum(-(1 << (255 * 8))}.should == get_erl("#{-(1 << (255 * 8)}") 58 | # get{@encoder.write_fixnum((1 << (512 * 8))}.should == get_erl("#{(1 << (512 * 8))}") 59 | # get{@encoder.write_fixnum(-((1 << (512 * 8)) - 1))}.should == get_erl("#{-((1 << (512 * 8)) - 1)}") 60 | # 61 | # write_any((1 << (255 * 8))).should == get_erl_with_magic("#{(1 << (255 * 8))}") 62 | # write_any(-(1 << (255 * 8)).should == get_erl_with_magic("#{-(1 << (255 * 8)}") 63 | # write_any((1 << (512 * 8))).should == get_erl_with_magic("#{(1 << (512 * 8))}") 64 | # write_any(-((1 << (512 * 8)) - 1)).should == get_erl_with_magic("#{-((1 << (512 * 8)) - 1)}") 65 | end 66 | 67 | # specify "A float (that is within the truncated precision of ruby compared to erlang) should encode as erlang does" do 68 | # get{@encoder.write_float 1.0}.should == get_erl("1.0") 69 | # get{@encoder.write_float -1.0}.should == get_erl("-1.0") 70 | # get{@encoder.write_float 123.456}.should == get_erl("123.456") 71 | # get{@encoder.write_float 123.456789012345}.should == get_erl("123.456789012345") 72 | # end 73 | 74 | specify "An Erlectiricity::NewReference should encode back to its original form" do 75 | ref_bin = run_erl("term_to_binary(make_ref())") 76 | ruby_ref = Erlectricity::Decoder.decode(ref_bin) 77 | 78 | get{@encoder.write_new_reference(ruby_ref)}.should == ref_bin[1..-1] 79 | write_any(ruby_ref).should == ref_bin 80 | end 81 | 82 | specify "An Erlectiricity::Pid should encode back to its original form" do 83 | pid_bin = run_erl("term_to_binary(spawn(fun() -> 3 end))") 84 | ruby_pid = Erlectricity::Decoder.decode(pid_bin) 85 | 86 | get{@encoder.write_pid(ruby_pid)}.should == pid_bin[1..-1] 87 | write_any(ruby_pid).should == pid_bin 88 | end 89 | 90 | specify "An array written with write_tuple should encode as erlang would a tuple" do 91 | get{@encoder.write_tuple [1,2,3]}.should == get_erl("{1,2,3}") 92 | get{@encoder.write_tuple [3] * 255}.should == get_erl("{#{([3] * 255).join(',')}}") 93 | get{@encoder.write_tuple [3] * 256}.should == get_erl("{#{([3] * 256).join(',')}}") 94 | get{@encoder.write_tuple [3] * 512}.should == get_erl("{#{([3] * 512).join(',')}}") 95 | end 96 | 97 | specify "An array should by default be written as a tuple" do 98 | write_any([1,2,3]).should == get_erl_with_magic("{1,2,3}") 99 | write_any([3] * 255).should == get_erl_with_magic("{#{([3] * 255).join(',')}}") 100 | write_any([3] * 256).should == get_erl_with_magic("{#{([3] * 256).join(',')}}") 101 | write_any([3] * 512).should == get_erl_with_magic("{#{([3] * 512).join(',')}}") 102 | end 103 | 104 | specify "An Erlectricity::List should by default be written as a list" do 105 | write_any(Erl::List.new([1,2,300])).should == get_erl_with_magic("[1,2,300]") 106 | write_any(Erl::List.new([300] * 255)).should == get_erl_with_magic("[#{([300] * 255).join(',')}]") 107 | write_any(Erl::List.new([300] * 256)).should == get_erl_with_magic("[#{([300] * 256).join(',')}]") 108 | write_any(Erl::List.new([300] * 512)).should == get_erl_with_magic("[#{([300] * 512).join(',')}]") 109 | end 110 | 111 | specify "An array written with write_list should encode as erlang would a list" do 112 | get{@encoder.write_list [1,2,300]}.should == get_erl("[1,2,300]") 113 | get{@encoder.write_list [300] * 255}.should == get_erl("[#{([300] * 255).join(',')}]") 114 | get{@encoder.write_list [300] * 256}.should == get_erl("[#{([300] * 256).join(',')}]") 115 | get{@encoder.write_list [300] * 512}.should == get_erl("[#{([300] * 512).join(',')}]") 116 | end 117 | 118 | specify "a string should be encoded as a erlang binary would be" do 119 | get{@encoder.write_binary "hey who"}.should == get_erl("<< \"hey who\" >>") 120 | get{@encoder.write_binary ""}.should == get_erl("<< \"\" >>") 121 | get{@encoder.write_binary "c\000c"}.should == get_erl("<< 99,0,99 >>") 122 | 123 | write_any("hey who").should == get_erl_with_magic("<< \"hey who\" >>") 124 | write_any("").should == get_erl_with_magic("<< \"\" >>") 125 | end 126 | 127 | def get 128 | @encoder.out = StringIO.new('', 'w') 129 | yield 130 | @encoder.out.string 131 | end 132 | 133 | def write_any(term) 134 | @encoder.out = StringIO.new('', 'w') 135 | @encoder.write_any term 136 | @encoder.out.string 137 | end 138 | 139 | def get_erl(str) 140 | get_erl_with_magic(str)[1..-1] #[1..-1] to chop off the magic number 141 | end 142 | 143 | def get_erl_with_magic(str) 144 | run_erl("term_to_binary(#{str.gsub(/"/, '\\\"')})") 145 | end 146 | end -------------------------------------------------------------------------------- /ext/decoder.c: -------------------------------------------------------------------------------- 1 | #include "ruby.h" 2 | #include 3 | 4 | #define ERL_VERSION 131 5 | #define ERL_SMALL_INT 97 6 | #define ERL_INT 98 7 | #define ERL_SMALL_BIGNUM 110 8 | #define ERL_LARGE_BIGNUM 111 9 | #define ERL_FLOAT 99 10 | #define ERL_ATOM 100 11 | #define ERL_REF 101 12 | #define ERL_NEW_REF 114 13 | #define ERL_PORT 102 14 | #define ERL_PID 103 15 | #define ERL_SMALL_TUPLE 104 16 | #define ERL_LARGE_TUPLE 105 17 | #define ERL_NIL 106 18 | #define ERL_STRING 107 19 | #define ERL_LIST 108 20 | #define ERL_BIN 109 21 | #define ERL_FUN 117 22 | #define ERL_NEW_FUN 112 23 | 24 | static VALUE mErlectricity; 25 | static VALUE cDecoder; 26 | void Init_decoder(); 27 | 28 | VALUE method_decode(VALUE klass, VALUE rString); 29 | 30 | VALUE read_any_raw(unsigned char **pData); 31 | 32 | // checkers 33 | 34 | void check_int(int num) { 35 | char buf[17]; 36 | sprintf(buf, "%u", num); 37 | rb_raise(rb_eStandardError, buf); 38 | } 39 | 40 | void check_str(char *str) { 41 | rb_raise(rb_eStandardError, str); 42 | } 43 | 44 | // string peekers/readers 45 | 46 | unsigned int peek_1(unsigned char **pData) { 47 | return (unsigned int) **pData; 48 | } 49 | 50 | unsigned int peek_2(unsigned char **pData) { 51 | return (unsigned int) ((**pData << 8) + *(*pData + 1)); 52 | } 53 | 54 | unsigned int peek_4(unsigned char **pData) { 55 | return (unsigned int) ((**pData << 24) + (*(*pData + 1) << 16) + (*(*pData + 2) << 8) + *(*pData + 3)); 56 | } 57 | 58 | unsigned int read_1(unsigned char **pData) { 59 | unsigned int val = peek_1(pData); 60 | *pData += 1; 61 | return val; 62 | } 63 | 64 | unsigned int read_2(unsigned char **pData) { 65 | unsigned int val = peek_2(pData); 66 | *pData += 2; 67 | return val; 68 | } 69 | 70 | unsigned int read_4(unsigned char **pData) { 71 | unsigned int val = peek_4(pData); 72 | *pData += 4; 73 | return val; 74 | } 75 | 76 | // tuples, lists 77 | 78 | VALUE read_small_tuple(unsigned char **pData) { 79 | if(read_1(pData) != ERL_SMALL_TUPLE) { 80 | rb_raise(rb_eStandardError, "Invalid Type, not a small tuple"); 81 | } 82 | 83 | int arity = read_1(pData); 84 | 85 | VALUE array = rb_ary_new2(arity); 86 | 87 | int i; 88 | for(i = 0; i < arity; ++i) { 89 | rb_ary_store(array, i, read_any_raw(pData)); 90 | } 91 | 92 | return array; 93 | } 94 | 95 | VALUE read_large_tuple(unsigned char **pData) { 96 | if(read_1(pData) != ERL_LARGE_TUPLE) { 97 | rb_raise(rb_eStandardError, "Invalid Type, not a large tuple"); 98 | } 99 | 100 | unsigned int arity = read_4(pData); 101 | 102 | VALUE array = rb_ary_new2(arity); 103 | 104 | int i; 105 | for(i = 0; i < arity; ++i) { 106 | rb_ary_store(array, i, read_any_raw(pData)); 107 | } 108 | 109 | return array; 110 | } 111 | 112 | VALUE read_list(unsigned char **pData) { 113 | if(read_1(pData) != ERL_LIST) { 114 | rb_raise(rb_eStandardError, "Invalid Type, not an erlang list"); 115 | } 116 | 117 | unsigned int size = read_4(pData); 118 | 119 | VALUE newref_class = rb_const_get(mErlectricity, rb_intern("List")); 120 | VALUE array = rb_funcall(newref_class, rb_intern("new"), 1, INT2NUM(size)); 121 | 122 | int i; 123 | for(i = 0; i < size; ++i) { 124 | rb_ary_store(array, i, read_any_raw(pData)); 125 | } 126 | 127 | read_1(pData); 128 | 129 | return array; 130 | } 131 | 132 | // primitives 133 | 134 | void read_string_raw(unsigned char *dest, unsigned char **pData, unsigned int length) { 135 | memcpy((char *) dest, (char *) *pData, length); 136 | *(dest + length) = (unsigned char) 0; 137 | *pData += length; 138 | } 139 | 140 | VALUE read_bin(unsigned char **pData) { 141 | if(read_1(pData) != ERL_BIN) { 142 | rb_raise(rb_eStandardError, "Invalid Type, not an erlang binary"); 143 | } 144 | 145 | unsigned int length = read_4(pData); 146 | 147 | VALUE rStr = rb_str_new((char *) *pData, length); 148 | *pData += length; 149 | 150 | return rStr; 151 | } 152 | 153 | VALUE read_string(unsigned char **pData) { 154 | if(read_1(pData) != ERL_STRING) { 155 | rb_raise(rb_eStandardError, "Invalid Type, not an erlang string"); 156 | } 157 | 158 | int length = read_2(pData); 159 | VALUE newref_class = rb_const_get(mErlectricity, rb_intern("List")); 160 | VALUE array = rb_funcall(newref_class, rb_intern("new"), 1, INT2NUM(length)); 161 | 162 | int i = 0; 163 | for(i; i < length; ++i) { 164 | rb_ary_store(array, i, INT2NUM(**pData)); 165 | *pData += 1; 166 | } 167 | 168 | return array; 169 | } 170 | 171 | VALUE read_atom(unsigned char **pData) { 172 | if(read_1(pData) != ERL_ATOM) { 173 | rb_raise(rb_eStandardError, "Invalid Type, not an atom"); 174 | } 175 | 176 | int length = read_2(pData); 177 | 178 | unsigned char buf[length + 1]; 179 | read_string_raw(buf, pData, length); 180 | 181 | // Erlang true and false are actually atoms 182 | if(length == 4 && strncmp((char *) buf, "true", length) == 0) { 183 | return Qtrue; 184 | } else if(length == 5 && strncmp((char *) buf, "false", length) == 0) { 185 | return Qfalse; 186 | } else { 187 | return ID2SYM(rb_intern((char *) buf)); 188 | } 189 | } 190 | 191 | VALUE read_small_int(unsigned char **pData) { 192 | if(read_1(pData) != ERL_SMALL_INT) { 193 | rb_raise(rb_eStandardError, "Invalid Type, not a small int"); 194 | } 195 | 196 | int value = read_1(pData); 197 | 198 | return INT2FIX(value); 199 | } 200 | 201 | VALUE read_int(unsigned char **pData) { 202 | if(read_1(pData) != ERL_INT) { 203 | rb_raise(rb_eStandardError, "Invalid Type, not an int"); 204 | } 205 | 206 | long long value = read_4(pData); 207 | 208 | long long negative = ((value >> 31) & 0x1 == 1); 209 | 210 | if(negative) { 211 | value = (value - ((long long) 1 << 32)); 212 | } 213 | 214 | return INT2FIX(value); 215 | } 216 | 217 | VALUE read_small_bignum(unsigned char **pData) { 218 | if(read_1(pData) != ERL_SMALL_BIGNUM) { 219 | rb_raise(rb_eStandardError, "Invalid Type, not a small bignum"); 220 | } 221 | 222 | unsigned int size = read_1(pData); 223 | unsigned int sign = read_1(pData); 224 | 225 | VALUE num = INT2NUM(0); 226 | VALUE tmp; 227 | 228 | unsigned char buf[size + 1]; 229 | read_string_raw(buf, pData, size); 230 | 231 | int i; 232 | for(i = 0; i < size; ++i) { 233 | tmp = INT2FIX(*(buf + i)); 234 | tmp = rb_funcall(tmp, rb_intern("<<"), 1, INT2NUM(i * 8)); 235 | num = rb_funcall(num, rb_intern("+"), 1, tmp); 236 | } 237 | 238 | if(sign) { 239 | num = rb_funcall(num, rb_intern("*"), 1, INT2NUM(-1)); 240 | } 241 | 242 | return num; 243 | } 244 | 245 | VALUE read_large_bignum(unsigned char **pData) { 246 | if(read_1(pData) != ERL_LARGE_BIGNUM) { 247 | rb_raise(rb_eStandardError, "Invalid Type, not a small bignum"); 248 | } 249 | 250 | unsigned int size = read_4(pData); 251 | unsigned int sign = read_1(pData); 252 | 253 | VALUE num = INT2NUM(0); 254 | VALUE tmp; 255 | 256 | unsigned char buf[size + 1]; 257 | read_string_raw(buf, pData, size); 258 | 259 | int i; 260 | for(i = 0; i < size; ++i) { 261 | tmp = INT2FIX(*(buf + i)); 262 | tmp = rb_funcall(tmp, rb_intern("<<"), 1, INT2NUM(i * 8)); 263 | 264 | num = rb_funcall(num, rb_intern("+"), 1, tmp); 265 | } 266 | 267 | if(sign) { 268 | num = rb_funcall(num, rb_intern("*"), 1, INT2NUM(-1)); 269 | } 270 | 271 | return num; 272 | } 273 | 274 | VALUE read_float(unsigned char **pData) { 275 | if(read_1(pData) != ERL_FLOAT) { 276 | rb_raise(rb_eStandardError, "Invalid Type, not a float"); 277 | } 278 | 279 | unsigned char buf[32]; 280 | read_string_raw(buf, pData, 31); 281 | 282 | VALUE rString = rb_str_new2((char *) buf); 283 | 284 | return rb_funcall(rString, rb_intern("to_f"), 0); 285 | } 286 | 287 | VALUE read_nil(unsigned char **pData) { 288 | if(read_1(pData) != ERL_NIL) { 289 | rb_raise(rb_eStandardError, "Invalid Type, not a nil list"); 290 | } 291 | 292 | VALUE newref_class = rb_const_get(mErlectricity, rb_intern("List")); 293 | return rb_funcall(newref_class, rb_intern("new"), 0); 294 | } 295 | 296 | // specials 297 | 298 | VALUE read_pid(unsigned char **pData) { 299 | if(read_1(pData) != ERL_PID) { 300 | rb_raise(rb_eStandardError, "Invalid Type, not a pid"); 301 | } 302 | 303 | VALUE node = read_atom(pData); 304 | VALUE id = INT2NUM(read_4(pData)); 305 | VALUE serial = INT2NUM(read_4(pData)); 306 | VALUE creation = INT2FIX(read_1(pData)); 307 | 308 | VALUE pid_class = rb_const_get(mErlectricity, rb_intern("Pid")); 309 | return rb_funcall(pid_class, rb_intern("new"), 4, node, id, serial, creation); 310 | } 311 | 312 | VALUE read_new_reference(unsigned char **pData) { 313 | if(read_1(pData) != ERL_NEW_REF) { 314 | rb_raise(rb_eStandardError, "Invalid Type, not a new-style reference"); 315 | } 316 | 317 | int size = read_2(pData); 318 | VALUE node = read_atom(pData); 319 | VALUE creation = INT2FIX(read_1(pData)); 320 | 321 | VALUE id = rb_ary_new2(size); 322 | int i; 323 | for(i = 0; i < size; ++i) { 324 | rb_ary_store(id, i, INT2NUM(read_4(pData))); 325 | } 326 | 327 | VALUE newref_class = rb_const_get(mErlectricity, rb_intern("NewReference")); 328 | return rb_funcall(newref_class, rb_intern("new"), 3, node, creation, id); 329 | } 330 | 331 | // read_any_raw 332 | 333 | VALUE read_any_raw(unsigned char **pData) { 334 | switch(peek_1(pData)) { 335 | case ERL_SMALL_INT: 336 | return read_small_int(pData); 337 | break; 338 | case ERL_INT: 339 | return read_int(pData); 340 | break; 341 | case ERL_FLOAT: 342 | return read_float(pData); 343 | break; 344 | case ERL_ATOM: 345 | return read_atom(pData); 346 | break; 347 | case ERL_PID: 348 | return read_pid(pData); 349 | break; 350 | case ERL_SMALL_TUPLE: 351 | return read_small_tuple(pData); 352 | break; 353 | case ERL_LARGE_TUPLE: 354 | return read_large_tuple(pData); 355 | break; 356 | case ERL_NIL: 357 | return read_nil(pData); 358 | break; 359 | case ERL_STRING: 360 | return read_string(pData); 361 | break; 362 | case ERL_LIST: 363 | return read_list(pData); 364 | break; 365 | case ERL_BIN: 366 | return read_bin(pData); 367 | break; 368 | case ERL_SMALL_BIGNUM: 369 | return read_small_bignum(pData); 370 | break; 371 | case ERL_LARGE_BIGNUM: 372 | return read_large_bignum(pData); 373 | break; 374 | case ERL_NEW_REF: 375 | return read_new_reference(pData); 376 | break; 377 | } 378 | return Qnil; 379 | } 380 | 381 | VALUE method_decode(VALUE klass, VALUE rString) { 382 | unsigned char *data = (unsigned char *) StringValuePtr(rString); 383 | 384 | unsigned char **pData = &data; 385 | 386 | // check protocol version 387 | if(read_1(pData) != ERL_VERSION) { 388 | rb_raise(rb_eStandardError, "Bad Magic"); 389 | } 390 | 391 | return read_any_raw(pData); 392 | } 393 | 394 | void Init_decoder() { 395 | mErlectricity = rb_const_get(rb_cObject, rb_intern("Erlectricity")); 396 | cDecoder = rb_define_class_under(mErlectricity, "Decoder", rb_cObject); 397 | rb_define_singleton_method(cDecoder, "decode", method_decode, 1); 398 | } 399 | --------------------------------------------------------------------------------