Skip to content

Commit

Permalink
JS-land proxies for Ruby objects. No support for GC or roundtripping.
Browse files Browse the repository at this point in the history
git-svn-id: svn+ssh://rubyforge.org/var/svn/johnson/trunk@100 54575175-8111-4fdf-a583-07ff49f40e23
  • Loading branch information
jbarnette committed Mar 31, 2008
1 parent e08d36d commit 69ace87
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 15 deletions.
21 changes: 16 additions & 5 deletions ext/spidermonkey/conversions.c
@@ -1,4 +1,5 @@
#include "conversions.h"
#include "js_proxy.h"
#include "ruby_proxy.h"

static jsval convert_float_or_bignum_to_js(OurContext* context, VALUE float_or_bignum)
Expand All @@ -13,7 +14,7 @@ static jsval convert_symbol_to_js(OurContext* context, VALUE symbol)
VALUE to_s = rb_funcall(symbol, rb_intern("to_s"), 0);
jsval name = STRING_TO_JSVAL(JS_NewStringCopyZ(context->js, StringValuePtr(to_s)));

// calls Ruby.symbolize(name) in JS-land. See prelude.js.
// calls Ruby.symbolize(name) in JS-land. See lib/prelude.js

jsval nsRuby;
assert(JS_GetProperty(context->js, context->global, "Ruby", &nsRuby) || JSVAL_VOID == nsRuby);
Expand All @@ -24,6 +25,11 @@ static jsval convert_symbol_to_js(OurContext* context, VALUE symbol)
return js;
}

static jsval convert_object_to_js(OurContext* context, VALUE object)
{
return make_js_proxy(context, object);
}

jsval convert_to_js(OurContext* context, VALUE ruby)
{
switch(TYPE(ruby))
Expand All @@ -50,14 +56,17 @@ jsval convert_to_js(OurContext* context, VALUE ruby)
case T_SYMBOL:
return convert_symbol_to_js(context, ruby);

case T_DATA:
case T_CLASS:
case T_OBJECT:
// FIXME: if it's a wrapped JS object, return it
return convert_object_to_js(context, ruby);

case T_DATA: // keep T_DATA last for fall-through
if (ruby_value_is_proxy(ruby))
return unwrap_ruby_proxy(context, ruby);

// UNIMPLEMENTED BELOW THIS LINE

case T_OBJECT:
case T_CLASS:
case T_FILE:
case T_STRUCT:
case T_MODULE:
Expand All @@ -77,7 +86,7 @@ static JSBool jsval_is_a_symbol(OurContext* context, jsval maybe_symbol)
jsval nsRuby, cSymbol;

assert(JS_GetProperty(context->js, context->global, "Ruby", &nsRuby));
if(JSVAL_VOID == nsRuby) Johnson_Error_raise("aaaaaaaugh!");
assert(JSVAL_VOID != nsRuby);

assert(JS_GetProperty(context->js, JSVAL_TO_OBJECT(nsRuby), "Symbol", &cSymbol));
assert(JSVAL_VOID != cSymbol);
Expand All @@ -103,6 +112,8 @@ VALUE convert_to_ruby(OurContext* context, jsval js)
if (jsval_is_a_symbol(context, js))
return ID2SYM(rb_intern(JS_GetStringBytes(JS_ValueToString(context->js, js))));

// FIXME: if it's wrapping a Ruby object, unwrap and return it

VALUE id = (VALUE)JS_HashTableLookup(context->ids, (void *)js);

if (id)
Expand Down
197 changes: 197 additions & 0 deletions ext/spidermonkey/js_proxy.c
@@ -0,0 +1,197 @@
#include "js_proxy.h"

static JSBool get(JSContext* js_context, JSObject* obj, jsval id, jsval* retval);
static void finalize(JSContext* context, JSObject* obj);
static JSBool set(JSContext* context, JSObject* obj, jsval id, jsval* retval);

static JSClass JSProxyClass = {
"JSProxy", JSCLASS_HAS_PRIVATE,
JS_PropertyStub,
JS_PropertyStub,
get,
set,
JS_EnumerateStub,
JS_ResolveStub,
JS_ConvertStub,
finalize
};

static void finalize(JSContext* context, JSObject* obj)
{
// FIXME
}
// void Johnson_RubyProxy_finalize(JSContext *js_context, JSObject *obj)
// {
// VALUE ruby;
// VALUE self = (VALUE)JS_GetContextPrivate(js_context);
//
// ruby = (VALUE)JS_GetInstancePrivate(js_context, obj, &gRubyProxyClass, NULL);
// VALUE c_name = rb_funcall(
// rb_funcall(self, rb_intern("class"), 0),
// rb_intern("to_s"), 0);
//
// if(rb_ivar_defined(self, rb_intern("@converted_objects"))) {
// rb_hash_delete( rb_iv_get(self, "@converted_objects"),
// rb_funcall(ruby, rb_intern("object_id"), 0));
// }
// }
//

static JSBool get(JSContext* js_context, JSObject* obj, jsval id, jsval* retval)
{
// pull out our Ruby object, which is embedded in js_context

VALUE ruby_context;
assert(ruby_context = (VALUE)JS_GetContextPrivate(js_context));

// get our struct, which is embedded in ruby_context

OurContext* context;
Data_Get_Struct(ruby_context, OurContext, context);

// get the Ruby object that backs this proxy

VALUE self;
assert(self = (VALUE)JS_GetInstancePrivate(context->js, obj, &JSProxyClass, NULL));

char* key = JS_GetStringBytes(JSVAL_TO_STRING(id));
VALUE ruby_id = rb_intern(key);

VALUE is_method = rb_funcall(self, rb_intern("respond_to?"), 1, ID2SYM(ruby_id));

if (is_method)
{
VALUE method = rb_funcall(self, rb_intern("method"), 1, ID2SYM(ruby_id));
int arity = NUM2INT(rb_funcall(method, rb_intern("arity"), 0));

// if the Ruby object has a 0-arity method named the same as the property
// we're trying to get, call it and return the converted result

if (arity == 0)
*retval = convert_to_js(context, rb_funcall(self, ruby_id, 0));
}
else
{
// otherwise, if the Ruby object quacks sorta like a hash (it responds to
// "[]" and "key?"), index it by key and return the converted result

VALUE is_indexable = rb_funcall(self, rb_intern("respond_to?"), 1, ID2SYM(rb_intern("[]")));
VALUE has_key_p = rb_funcall(self, rb_intern("respond_to?"), 1, ID2SYM(rb_intern("key?")));

if (is_indexable && has_key_p)
*retval = convert_to_js(context, rb_funcall(self, rb_intern("[]"), 1, rb_str_new2(key)));
}

return JS_TRUE;
}

static JSBool set(JSContext* js_context, JSObject* obj, jsval id, jsval* value)
{
VALUE ruby_context;
assert(ruby_context = (VALUE)JS_GetContextPrivate(js_context));

OurContext* context;
Data_Get_Struct(ruby_context, OurContext, context);

VALUE self;
assert(self = (VALUE)JS_GetInstancePrivate(context->js, obj, &JSProxyClass, NULL));

char* key = JS_GetStringBytes(JSVAL_TO_STRING(id));
VALUE ruby_key = rb_str_new2(key);

VALUE setter = rb_str_append(rb_str_new3(ruby_key), rb_str_new2("="));
VALUE setter_id = rb_intern(StringValuePtr(setter));

VALUE has_setter = rb_funcall(self, rb_intern("respond_to?"), 1, ID2SYM(setter_id));

if (has_setter)
{
VALUE method = rb_funcall(self, rb_intern("method"), 1, ID2SYM(setter_id));
int arity = NUM2INT(rb_funcall(method, rb_intern("arity"), 0));

// if the Ruby object has a 1-arity method named "property=",
// call it with the converted value

if (arity == 1)
rb_funcall(self, setter_id, 1, convert_to_ruby(context, *value));
}
else
{
// otherwise, if the Ruby object quacks sorta like a hash for assignment
// (it responds to "[]="), assign it by key

VALUE is_index_assignable =
rb_funcall(self, rb_intern("respond_to?"), 1, ID2SYM(rb_intern("[]=")));

if (is_index_assignable)
rb_funcall(self, rb_intern("[]="), 2, ruby_key, convert_to_ruby(context, *value));
}

return JS_TRUE;
}

static JSBool method_missing(JSContext* js_context, JSObject* obj, uintN argc, jsval* argv, jsval* retval)
{
VALUE ruby_context;
assert(ruby_context = (VALUE)JS_GetContextPrivate(js_context));

OurContext* context;
Data_Get_Struct(ruby_context, OurContext, context);

VALUE self;
assert(self = (VALUE)JS_GetInstancePrivate(context->js, obj, &JSProxyClass, NULL));

char* key = JS_GetStringBytes(JSVAL_TO_STRING(argv[0]));

VALUE ruby_id = rb_intern(key);

// FIXME: this could probably be a lot faster, to_a comes from enumerable on proxy
VALUE args = rb_funcall(convert_to_ruby(context, argv[1]), rb_intern("to_a"), 0);

// Context#jsend: if the last arg is a function, it'll get passed along as a &block

*retval = convert_to_js(context,
rb_funcall(ruby_context, rb_intern("jsend"), 3, self, ID2SYM(ruby_id), args));

return JS_TRUE;
}

JSBool js_value_is_proxy(jsval maybe_proxy)
{
return JS_FALSE;
}

VALUE unwrap_js_proxy(OurContext* context, jsval proxy)
{
return Qnil;
}

// static jsval convert_ruby_object_to_jsval(CombinedContext* context, VALUE ruby)
// {
// VALUE self = (VALUE)JS_GetContextPrivate(context->js);
// JSObject * js = JS_NewObject(context->js, &gRubyProxyClass, NULL, NULL);
// if(!js) Johnson_Error_raise("failed JS_NewObject");
//
// rb_hash_aset( rb_iv_get(self, "@converted_objects"),
// rb_funcall(ruby, rb_intern("object_id"), 0),
// ruby );
//
// if(JS_SetPrivate(context->js, js, (void*)ruby) == JS_FALSE)
// Johnson_Error_raise("failed JS_SetPrivate");
//
// JS_DefineFunction(context->js, js, "__noSuchMethod__",
// Johnson_RubyProxy_method_missing, 2, 0);
//
// return OBJECT_TO_JSVAL(js);
// }

jsval make_js_proxy(OurContext* context, VALUE value)
{
JSObject *js;

assert(js = JS_NewObject(context->js, &JSProxyClass, NULL, NULL));
assert(JS_SetPrivate(context->js, js, (void*)value));
assert(JS_DefineFunction(context->js, js, "__noSuchMethod__", method_missing, 2, 0));

return OBJECT_TO_JSVAL(js);
}
11 changes: 11 additions & 0 deletions ext/spidermonkey/js_proxy.h
@@ -0,0 +1,11 @@
#ifndef JOHNSON_SPIDERMONKEY_JS_PROXY_H
#define JOHNSON_SPIDERMONKEY_JS_PROXY_H

#include "spidermonkey.h"
#include "context.h"

JSBool js_value_is_proxy(jsval maybe_proxy);
VALUE unwrap_js_proxy(OurContext* context, jsval proxy);
jsval make_js_proxy(OurContext* context, VALUE value);

#endif
2 changes: 1 addition & 1 deletion ext/spidermonkey/ruby_proxy.c
Expand Up @@ -268,7 +268,7 @@ jsval unwrap_ruby_proxy(OurContext* context, VALUE wrapped)

void init_Johnson_SpiderMonkey_Proxy(VALUE spidermonkey)
{
proxy_class = rb_define_class_under(spidermonkey, "Proxy", rb_cObject);
proxy_class = rb_define_class_under(spidermonkey, "RubyProxy", rb_cObject);

rb_define_method(proxy_class, "[]", get, 1);
rb_define_method(proxy_class, "[]=", set, 2);
Expand Down
2 changes: 1 addition & 1 deletion lib/johnson.rb
Expand Up @@ -12,7 +12,7 @@

# the SpiderMonkey bits written in Ruby
require "johnson/spidermonkey/context"
require "johnson/spidermonkey/proxy"
require "johnson/spidermonkey/ruby_proxy"
require "johnson/spidermonkey/mutable_tree_visitor"
require "johnson/spidermonkey/immutable_node"

Expand Down
12 changes: 12 additions & 0 deletions lib/johnson/spidermonkey/context.rb
@@ -1,6 +1,18 @@
module Johnson #:nodoc:
module SpiderMonkey #:nodoc:
class Context # native
def initialize
@gcthings = {}
end

def jsend(target, symbol, args)
if args.last && args.last.is_a?(RubyProxy) && args.last.function?
block = args.pop
target.__send__(symbol, *args, &block)
else
target.__send__(symbol, *args)
end
end
end
end
end
@@ -1,6 +1,6 @@
module Johnson #:nodoc:
module SpiderMonkey #:nodoc:
class Proxy # native
class RubyProxy # native
include Enumerable

def initialize
Expand Down
3 changes: 2 additions & 1 deletion test/helper.rb
Expand Up @@ -13,7 +13,7 @@

module Johnson
class TestCase < Test::Unit::TestCase
def default_test; end
undef :default_test

def assert_js(expression, options={})
context = options[:context] || @context
Expand All @@ -30,6 +30,7 @@ class NodeTestCase < Test::Unit::TestCase
include Johnson::Nodes

undef :default_test

def setup
@parser = Johnson::Parser
end
Expand Down
21 changes: 21 additions & 0 deletions test/johnson/spidermonkey/context_test.rb
Expand Up @@ -5,13 +5,34 @@ module SpiderMonkey
class ContextTest < Johnson::TestCase
def setup
@context = Johnson::SpiderMonkey::Context.new
@context.evaluate(Johnson::PRELUDE)
end

def test_provides_basic_context_interface
assert(@context.respond_to?(:evaluate))
assert(@context.respond_to?(:[]))
assert(@context.respond_to?(:[]=))
end

class Foo
def gets_an_unspecified_block
block_given?
end

def runs_block(arg, &block)
yield(arg)
end
end

def test_jsend_deals_with_blocks
func = @context.evaluate("function() {}")
assert(@context.jsend(Foo.new, :gets_an_unspecified_block, [func]))
end

def test_jsend_deals_with_specified_blocks
func = @context.evaluate("function(x) { return x * 2 }")
assert_equal(4, @context.jsend(Foo.new, :runs_block, [2, func]))
end
end
end
end

0 comments on commit 69ace87

Please sign in to comment.