Skip to content

Commit

Permalink
Start prototyping user-definable Java<=>Ruby conversions.
Browse files Browse the repository at this point in the history
  • Loading branch information
headius committed Mar 11, 2015
1 parent 9ac895a commit c8ea8bf
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 6 deletions.
14 changes: 14 additions & 0 deletions core/src/main/java/org/jruby/RubyBasicObject.java
Expand Up @@ -668,6 +668,15 @@ public Class getJavaClass() {
return getClass();
}

/**
* @see IRubyObject#canCoerceTo(Class)
*/
@Override
public boolean canCoerceTo(Class target) {
return target.equals(getJavaClass()) ||
getRuntime().getJavaSupport().getRegisteredConverter(getMetaClass().getRealClass(), target) != null;
}

/** rb_to_id
*
* Will try to convert this object to a String using the Ruby
Expand Down Expand Up @@ -835,6 +844,11 @@ public Object toJava(Class target) {
}
} else if (target.isAssignableFrom(getClass())) {
return this;
} else {
JavaUtil.RubyToJava converter = getRuntime().getJavaSupport().getRegisteredConverter(getMetaClass().getRealClass(), target);
if (converter != null) {
return converter.convert(getRuntime().getCurrentContext(), this);
}
}

throw getRuntime().newTypeError("cannot convert instance of " + getClass() + " to " + target);
Expand Down
7 changes: 6 additions & 1 deletion core/src/main/java/org/jruby/RubyNumeric.java
Expand Up @@ -1208,7 +1208,12 @@ public IRubyObject conjugate(ThreadContext context) {

@Override
public Object toJava(Class target) {
return JavaUtil.getNumericConverter(target).coerce(this, target);
JavaUtil.NumericConverter converter = JavaUtil.getNumericConverter(target);
if (converter == JavaUtil.NUMERIC_TO_OTHER) {
return super.toJava(target);
}

return converter.coerce(this, target);
}

public static class InvalidIntegerException extends NumberFormatException {
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/org/jruby/ir/operands/UndefinedValue.java
Expand Up @@ -399,4 +399,9 @@ public String toString() {
public void visit(IRVisitor visitor) {
visitor.UndefinedValue(this);
}

@Override
public boolean canCoerceTo(Class target) {
return false;
}
}
Expand Up @@ -295,8 +295,13 @@ private static boolean exactMatch(ParameterTypes paramTypes, IRubyObject... args

private static Matcher EXACT = new Matcher() {
public boolean match(Class type, IRubyObject arg) {
return type.equals(argClass(arg))
|| (type.isPrimitive() && CodegenUtils.getBoxType(type) == argClass(arg));
if (arg == null) {
if (type.isPrimitive()) return false;
return true;
} else if (arg.canCoerceTo(type)) {
return true;
}
return false;
}
};

Expand Down
6 changes: 6 additions & 0 deletions core/src/main/java/org/jruby/javasupport/JavaSupport.java
Expand Up @@ -111,4 +111,10 @@ public abstract class JavaSupport {
public abstract ClassValue<Map<String, AssignedName>> getStaticAssignedNames();

public abstract ClassValue<Map<String, AssignedName>> getInstanceAssignedNames();

public abstract void registerConverter(RubyClass source, Class target, JavaUtil.RubyToJava converter);

public abstract void registerConverter(RubyClass source, Class target, IRubyObject converter);

public abstract JavaUtil.RubyToJava getRegisteredConverter(RubyClass source, Class target);
}
44 changes: 44 additions & 0 deletions core/src/main/java/org/jruby/javasupport/JavaSupportImpl.java
Expand Up @@ -33,7 +33,14 @@
***** END LICENSE BLOCK *****/
package org.jruby.javasupport;

import org.jruby.java.proxies.JavaProxy;
import org.jruby.javasupport.binding.AssignedName;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.callsite.CachingCallSite;
import org.jruby.runtime.callsite.FunctionalCachingCallSite;
import org.jruby.runtime.opto.Invalidator;
import org.jruby.runtime.opto.OptoFactory;
import org.jruby.util.TypeConverter;
import org.jruby.util.collections.MapBasedClassValue;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -42,6 +49,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.jruby.Ruby;
import org.jruby.RubyClass;
Expand Down Expand Up @@ -75,6 +83,8 @@ public IRubyObject allocateProxy(Object javaObject, RubyClass clazz) {
private final ClassValue<Map<String, AssignedName>> staticAssignedNames;
private final ClassValue<Map<String, AssignedName>> instanceAssignedNames;
private static final Constructor<? extends ClassValue> CLASS_VALUE_CONSTRUCTOR;
private final Map<RubyClass, Map<Class, JavaUtil.RubyToJava>> registeredConverters;
private final Invalidator registeredConverterInvalidator;

static {
Constructor constructor = null;
Expand Down Expand Up @@ -167,6 +177,9 @@ public Map<String, AssignedName> computeValue(Class<?> cls) {
catch (InvocationTargetException ex) {
throw new RuntimeException(ex.getTargetException());
}

registeredConverters = new WeakHashMap<>();
registeredConverterInvalidator = OptoFactory.newConstantInvalidator();
}

public Class loadJavaClass(String className) throws ClassNotFoundException {
Expand Down Expand Up @@ -372,6 +385,37 @@ public ClassValue<Map<String, AssignedName>> getInstanceAssignedNames() {
return instanceAssignedNames;
}

public synchronized void registerConverter(RubyClass source, Class target, JavaUtil.RubyToJava converter) {
Map<Class, JavaUtil.RubyToJava> classToConverter = registeredConverters.get(source);
if (classToConverter == null) {
registeredConverters.put(source, classToConverter = new WeakHashMap<Class, JavaUtil.RubyToJava>());
}
classToConverter.put(target, converter);

registeredConverterInvalidator.invalidate();
}

public synchronized JavaUtil.RubyToJava getRegisteredConverter(RubyClass source, Class target) {
Map<Class, JavaUtil.RubyToJava> classToConverter = registeredConverters.get(source);
if (classToConverter == null) return null;

return classToConverter.get(target);
}

public void registerConverter(RubyClass source, Class target, final IRubyObject converter) {
JavaUtil.RubyToJava rubyToJava = new JavaUtil.RubyToJava() {
CachingCallSite site = new FunctionalCachingCallSite("call");
@Override
public Object convert(ThreadContext context, IRubyObject object) {
Object result = site.call(runtime.getCurrentContext(), converter, converter, object);
if (result instanceof JavaProxy) result = ((JavaProxy)result).getObject();
return result;
}
};

registerConverter(source, target, rubyToJava);
}

@Deprecated
private volatile Map<Object, Object[]> javaObjectVariables;

Expand Down
14 changes: 11 additions & 3 deletions core/src/main/java/org/jruby/javasupport/JavaUtil.java
Expand Up @@ -475,13 +475,21 @@ public static Set<String> getRubyNamesForJavaName(String javaName, List<Method>

return nameSet;
}

public interface RubyToJava {
public Object convert(ThreadContext context, IRubyObject object);
}

public static abstract class JavaConverter {
private final Class type;
public JavaConverter(Class type) {this.type = type;}
public abstract IRubyObject convert(Ruby runtime, Object object);
public abstract IRubyObject get(Ruby runtime, Object array, int i);
public abstract void set(Ruby runtime, Object array, int i, IRubyObject value);
public IRubyObject get(Ruby runtime, Object array, int i) {
return convert(runtime, Array.get(array, i));
}
public void set(Ruby runtime, Object array, int i, IRubyObject value) {
Array.set(array, i, value.toJava(type));
}
public String toString() {return type.getName() + " converter";}
}

Expand Down Expand Up @@ -899,7 +907,7 @@ public Object coerce(RubyNumeric numeric, Class target) {
}
}
};
private static final NumericConverter NUMERIC_TO_OTHER = new NumericConverter() {
public static final NumericConverter NUMERIC_TO_OTHER = new NumericConverter() {
public Object coerce(RubyNumeric numeric, Class target) {
if (target.isAssignableFrom(numeric.getClass())) {
// just return as-is, since we can't do any coercion
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/java/org/jruby/javasupport/JavaUtilities.java
@@ -1,7 +1,10 @@
package org.jruby.javasupport;

import org.jruby.RubyClass;
import org.jruby.RubyProc;
import org.jruby.anno.JRubyMethod;
import org.jruby.anno.JRubyModule;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
Expand Down Expand Up @@ -53,4 +56,27 @@ public static IRubyObject get_top_level_proxy_or_package(ThreadContext context,
public static IRubyObject get_proxy_or_package_under_package(ThreadContext context, IRubyObject recv, IRubyObject arg0, IRubyObject arg1) {
return Java.get_proxy_or_package_under_package(context, recv, arg0, arg1);
}

@JRubyMethod(module = true, visibility = Visibility.PRIVATE)
public static IRubyObject register_converter(ThreadContext context, IRubyObject recv, IRubyObject source, IRubyObject target, IRubyObject converter, Block block) {
if (block.isGiven()) {
context.runtime.getWarnings().warn("block passed to `reigster_converter' ignored");
}

if (!(source instanceof RubyClass)) {
throw context.runtime.newTypeError(source, context.runtime.getClassClass());
}

Object maybeTarget = target.dataGetStruct();
if (!(maybeTarget instanceof Class)) context.runtime.newTypeError(target, Java.getProxyClass(context.runtime, Class.class));

context.runtime.getJavaSupport().registerConverter((RubyClass)source, (Class)maybeTarget, converter);

return context.nil;
}

@JRubyMethod(module = true, visibility = Visibility.PRIVATE)
public static IRubyObject register_converter(ThreadContext context, IRubyObject recv, IRubyObject source, IRubyObject target, Block block) {
return register_converter(context, recv, source, target, RubyProc.newProc(context.runtime, block, Block.Type.LAMBDA), Block.NULL_BLOCK);
}
}
5 changes: 5 additions & 0 deletions core/src/main/java/org/jruby/runtime/builtin/IRubyObject.java
Expand Up @@ -186,6 +186,11 @@ public interface IRubyObject {
* @return Class
*/
Class getJavaClass();

/**
* Return true if this object can coerce to the given target Java class, false otherwise.
*/
boolean canCoerceTo(Class target);

/**
* Convert the object into a symbol name if possible.
Expand Down

0 comments on commit c8ea8bf

Please sign in to comment.