diff --git a/lib/v8.rb b/lib/v8.rb index c2f6821..69e50e5 100644 --- a/lib/v8.rb +++ b/lib/v8.rb @@ -6,7 +6,6 @@ require 'v8/context' # require 'v8/error' # require 'v8/stack' require 'v8/conversion/fundamental' -# require 'v8/conversion/indentity' # require 'v8/conversion/reference' # require 'v8/conversion/primitive' # require 'v8/conversion/code' diff --git a/lib/v8/conversion.rb b/lib/v8/conversion.rb index 64a1a36..814f970 100644 --- a/lib/v8/conversion.rb +++ b/lib/v8/conversion.rb @@ -1,23 +1,85 @@ +## +# An extensible conversion mechanism for converting objects to and +# from their V8 representations. +# +# The Ruby Racer has two levels of representation for JavaScript +# objects: the low-level C++ objects which are just thin wrappers +# native counterparts. These are the objects in the `V8::C::*` +# namespace. It also has high-level Ruby objects which can +# either be instances of `V8::Object`, `V8::Date`, or just plain Ruby +# objects that have representations in the JavaScript runtime. +# +# The conversion object held by the context captures this transition +# from one object type to the other. It is implemented as a "middleware" +# stack where at the base is the `Fundamental` conversion which does +# basic conversion and identity mapping. +# +# In order to "extend" or override the conversion mechanism, you can +# extend this object to add behaviors. For example, the following +# extension will add a `__fromRuby__` property to every ruby object +# that is embedded into this context. +# +# module TagRubyObjects +# def to_v8(context, ruby_object) +# super.tap do |v8_object| +# v8_object.Set(V8::C::String::NewFromUtf8("__fromRuby__"), V8::C::Boolean::New(true)) +# end +# end +# end +# context.conversion.extend TagRubyObjects +# +# @see V8::Conversion::Fundamental for the basic conversion operation. class V8::Conversion include Fundamental - # include Identity + + ## + # Convert a low-level instance of `V8::C::Value` or one of its + # subclasses into a Ruby object. + # + # The `Fundamental` conversion will call `v8_object.to_ruby`, but + # any number of middlewares can be inserted between then. + # + # @param [V8::C::Value] the object to convert + # @return [Object] the corresponding Ruby value def to_ruby(v8_object) super v8_object end + ## + # Convert a Ruby Object into a low-level `V8::C::Value` or one of + # its subclasses. Note here that things like `V8::Object` are + # considered Ruby objects and *not* V8 objects. So, for example, the + # fundamental conversion for `V8::Object` will return a + # `V8::C::Object` + # + # The `Fundamental` conversion will call + # `ruby_object.to_v8(context)` optionally storing the result in an + # identity map in the case where the result is a `V8::C::Object` + # + # @param [V8::Context] the Ruby context in the conversion happens + # @param [Object] Ruby object to convert + # @return [V8::C::Value] the v8 representation def to_v8(context, ruby_object) super context, ruby_object end end + +## +# The folowing represent the default conversions from instances of +# `V8::C::Value` into their Ruby counterparts. module V8::C class String - alias_method :to_ruby, :Utf8Value + def to_ruby + self.Utf8Value() + end end class Number - alias_method :to_ruby, :Value + def to_ruby + self.Value() + end end class Undefined @@ -51,6 +113,9 @@ module V8::C end end +## +# The following are the default conversions from Ruby objects into +# low-level C++ objects. class String def to_v8(context) V8::C::String::NewFromUtf8(context.isolate.native, self) @@ -69,25 +134,3 @@ class Symbol V8::C::Symbol::For(context.isolate.native, V8::C::String::NewFromUtf8(isolate, to_s)) end end - -# for type in [TrueClass, FalseClass, NilClass, Float] do -# type.class_eval do -# include V8::Conversion::Primitive -# end -# end - -# for type in [Class, Object, Array, Hash, String, Symbol, Time, Proc, Method, Fixnum] do -# type.class_eval do -# include V8::Conversion.const_get(type.name) -# end -# end - -# class UnboundMethod -# include V8::Conversion::Method -# end - -# for type in [:Object, :String, :Date] do -# V8::C::const_get(type).class_eval do -# include V8::Conversion::const_get("Native#{type}") -# end -# end diff --git a/lib/v8/conversion/fundamental.rb b/lib/v8/conversion/fundamental.rb index 66fd272..9e780b8 100644 --- a/lib/v8/conversion/fundamental.rb +++ b/lib/v8/conversion/fundamental.rb @@ -1,11 +1,77 @@ +require 'v8/weak' + class V8::Conversion + ## + # This is the fundamental conversion for from Ruby objects into + # `V8::C::Value`s and vice-versa. For instances of `V8::C::Object`, + # the result is stored in an identity map, so that subsequent + # conversions return the same object both to and from ruby. + # + # It sits at the top of the conversion "middleware stack" module Fundamental + ## + # Convert a low-level `V8::C::Value` into a Ruby object, + # optionally storing the result in an identity map so that it can + # be re-used for subsequent conversions. + # + # @param [V8::C::Value] low-level C++ object. + # @return [Object] Ruby object counterpart def to_ruby(v8_object) - v8_object.to_ruby + # Only objects can be reliably identified. If this is an + # object, then we want to see if there is already a ruby object + # associated with it. + if v8_object.kind_of? V8::C::Object + if rb_object = v8_idmap[v8_object.GetIdentityHash()] + # it was in the id map, so return the existing instance. + rb_object + else + # it was not in the id map, so we run the default conversion + # and store it in the id map + v8_object.to_ruby.tap do |object| + equate object, v8_object + end + end + else + # not an object, just do the default conversion + v8_object.to_ruby + end end + ## + # Convert a Ruby object into a low-level C++ `V8::C::Value`. + # + # First it checks to see if there is an entry in the id map for + # this object. Otherwise, it will run the default conversion. def to_v8(context, ruby_object) - ruby_object.to_v8 context + rb_idmap[ruby_object.object_id] || ruby_object.to_v8(context) + end + + ## + # Mark a ruby object and a low-level V8 C++ object as being + # equivalent in this context. + # + # After being equated the two objects are like mirrors of each + # other, where one exists in the Ruby world, and the other exists + # in the V8 world. It's all very through the looking glass. + # + # Whenever `ruby_object` is reflected into the V8 runtime, then + # `v8_object` will be used in its stead, and whenever `v8_object` + # is reflected into the Ruby runtime, then `ruby_object` will be + # used in *its* stead. + # + # @param [Object] the ruby object + # @param [V8::C::Value] the v8 object + def equate(ruby_object, v8_object) + v8_idmap[v8_object.GetIdentityHash()] = ruby_object + rb_idmap[ruby_object.object_id] = v8_object + end + + def v8_idmap + @v8_idmap ||= V8::Weak::WeakValueMap.new + end + + def rb_idmap + @ruby_idmap ||= V8::Weak::WeakValueMap.new end end end diff --git a/lib/v8/conversion/indentity.rb b/lib/v8/conversion/indentity.rb deleted file mode 100644 index f566f95..0000000 --- a/lib/v8/conversion/indentity.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'ref' - -class V8::Conversion - module Identity - def to_ruby(v8_object) - if v8_object.class <= V8::C::Object - v8_idmap[v8_object.GetIdentityHash()] || super(v8_object) - else - super(v8_object) - end - end - - def to_v8(ruby_object) - return super(ruby_object) if ruby_object.is_a?(String) || ruby_object.is_a?(Primitive) - rb_idmap[ruby_object.object_id] || super(ruby_object) - end - - def equate(ruby_object, v8_object) - v8_idmap[v8_object.GetIdentityHash()] = ruby_object - rb_idmap[ruby_object.object_id] = v8_object - end - - def v8_idmap - @v8_idmap ||= V8::Weak::WeakValueMap.new - end - - def rb_idmap - @ruby_idmap ||= V8::Weak::WeakValueMap.new - end - end -end \ No newline at end of file diff --git a/lib/v8/object.rb b/lib/v8/object.rb index 83123e3..95e7c29 100644 --- a/lib/v8/object.rb +++ b/lib/v8/object.rb @@ -4,8 +4,7 @@ class V8::Object def initialize(native = nil) @context = V8::Context.current or fail "tried to initialize a #{self.class} without being in an entered V8::Context" - @native = block_given? ? yield : native || V8::C::Object::New() - #@context.link self, @native + @native = native || V8::C::Object::New(@context.isolate.native) end def [](key) diff --git a/spec/v8/context_spec.rb b/spec/v8/context_spec.rb index 732ef11..cf15900 100644 --- a/spec/v8/context_spec.rb +++ b/spec/v8/context_spec.rb @@ -100,12 +100,12 @@ describe "V8::Context" do # end # end - # xit "always returns the same ruby object for a single javascript object" do - # obj = @cxt.eval('obj = {}') - # obj.should be(@cxt['obj']) - # @cxt.eval('obj').should be(@cxt['obj']) - # @cxt['obj'].should be(@cxt['obj']) - # end + it "always returns the same ruby object for a single javascript object" do + obj = @cxt.eval('obj = {}') + obj.should be(@cxt['obj']) + @cxt.eval('obj').should be(@cxt['obj']) + @cxt['obj'].should be(@cxt['obj']) + end # xit "converts arrays to javascript" do # @cxt['a'] = [1,2,4]