Skip to main content

JSR 292 Goodness: named parameters

Posted by forax on January 21, 2011 at 10:32 AM PST

Today, I want to show you a way to implement method invocation with named parameters using JSR 292.
But before using JSR 292 API, we need a way to reflect the parameter names of any existing methods.
The problem is that java.lang.reflect doesn't provide any way to get those parameter names,
so I had to first write a small class that does reflection of parameter names using my second favorite API, ASM.

  public static List<MethodInfo> getMethods(final Class<?> clazz) throws IOException {
    String className = clazz.getName().replace('.', '/') + ".class";
    final ClassLoader classLoader = clazz.getClassLoader();
   
    final ArrayList methods = new ArrayList<>();  // thanks Joe
    try(InputStream input = (classLoader != null)?  // thanks Joe
          classLoader.getResourceAsStream(className):
           ClassLoader.getSystemResourceAsStream(className)) {
     
      ClassReader reader = new ClassReader(input);
      reader.accept(new EmptyVisitor(){
        @Override
        public MethodVisitor visitMethod(final int access, final String name, String desc, String signature, String[] exceptions) {
          final MethodType methodType = MethodType.fromMethodDescriptorString(desc, classLoader);
          int parameterCount = methodType.parameterCount();
          final String[] parameterNames = new String[parameterCount];
         
          if (parameterCount == 0) { // shortcut for method with no parameter
            methods.add(new MethodInfo(access, clazz, name, methodType, parameterNames));
            return null;
          }
         
          return new EmptyVisitor() {
            @Override
            public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
              if (index>= parameterNames.length || parameterNames[index] != null) {
                return;
              }
              parameterNames[index] = name;
            }
           
            @Override
            public void visitEnd() {
              for(int i=0; i<parameterNames.length; i++) {
                if (parameterNames[i] == null) {
                  parameterNames[i] = "args" + i;
                }
              }
             
              methods.add(new MethodInfo(access, clazz, name, methodType, parameterNames));
            }
          };
        }
      }, ClassReader.SKIP_FRAMES);
     
      return methods;
    }

This code is not very efficient, it will be more efficient to lookup and decode the code attribute LocalVariableTable, anyway it does the job.

Otherwise, I think my co-worker had serious doubt about my mental sanity because I thank Joe loudly (the sound has to cross the Atlantic ocean) every 20 lines of codes,
his project Coin really simplifies my day to day job. We should start a Joe Thanker Club !

Named parameters

Now, let suppose I want to resolve MyClass.foo(s: "eleven", i:7) knowing that foo is declared like that:

    public static String foo(int i, String s) {
        return i + s;
    }
 

One can notice that the names of the parameters are constant. The permutation can't be processed at compile time because we want that the declaration can be modified and recompiled independently of its usage.
Also instead of doing the permutation at each call, we can process the corresponding permutation of parameters once at linked time. Let's implement that with invokedynamic.

Calling foo is equivalent to this invokedynamic call

  invokedynamic [#bsm, MyClass.class, "s", "i"] foo("eleven", 7)

but because Java the language has no syntax for invokedynamic, I will use the class DynamicIndy that I had previously introduced. I've just updated it to use a newer beta of ASM4.

MethodHandle mh = new DynamicIndy().invokeDynamic("foo",
  MethodType.methodType(String.class, String.class, int.class),
  Main.class,
  "bsm",
  MethodType.methodType(CallSite.class, Lookup.class, String.class, MethodType.class, Object[].class),
  Main.class, "s", "i");
System.out.println((String)mh.invokeExact("eleven", 7));

Writting the boostrap method is straightforward knowing that Methodhandles has a method permuteArguments. To avoid to do the reflection more than once by class. The result of a reflection is cached.
Instead of using a homemade concurrent WeakHashMap and to try to workaround its defects (4429536, 6389107),
I use a new class introduced by JSR 292, ClassValue that safetly associates to a class a value computed on demand and cached.

  private final static ClassValue<HashMap<String, MethodInfo>> methodsClassValue =
      new ClassValue<>() {   // thanks Joe
        @Override
        protected HashMap<String, MethodInfo> computeValue(Class<?> type) {
          List methods;
          try {
            methods = MethodInfo.getMethods(type);
          } catch(IOException e) {
            throw (LinkageError)new LinkageError().initCause(e);
          }
          HashMap<String, MethodInfo> methodMap = new HashMap<>();  // thanks Joe
          for(MethodInfo method: methods) {
            methodMap.put(method.getName(), method);
          }
          return methodMap;
        }
      };
     
  public static CallSite bsm(Lookup lookup, String name, MethodType methodType, Object... bsmArgs) throws Throwable {
    HashMap<String, MethodInfo> methodMap = methodsClassValue.get((Class<?>)bsmArgs[0]);
    MethodInfo method = methodMap.get(name);
    if (method == null) {
      throw new InvokeDynamicBootstrapError("no method "+name+" found");
    }
   
    int parameterCount = method.getParameterCount();
    if (parameterCount != methodType.parameterCount()) {
      throw new InvokeDynamicBootstrapError("wrong number of parameters "+methodType+                                             " for method "+method);
    }
   
    HashMap<String,Integer> parametersMap = new HashMap<>();   // thanks Joe
    for(int i=0; i < parameterCount; i++) {
      parametersMap.put(method.getParameterName(i), i);
    }
   
    int[] permutation = new int[parameterCount];
    for(int i=0; i < parameterCount; i++) {
      Object parameterName = bsmArgs[i + 1];
      Integer slot = parametersMap.get(parameterName);
      if (slot == null) {
        throw new InvokeDynamicBootstrapError("unknown parameter name "+parameterName);
      }
      permutation[i] = slot;
    }
   
    MethodHandle mh = MethodHandles.permuteArguments(
      method.asMethodHandle(lookup), methodType, permutation);
    return new ConstantCallSite(mh);
  }

The code is freely available, see the attachment.

That's all folks, at least for today,
Rémi

Related Topics >>

Comments

JSR 292 Goodness: named

Are you aware that both bugs are now marked as "not available"? Are they security-related?

JSR 292 Goodness: named

Weird, these bugs aren't, as far as I know, related to some security stuff.
There is a ghost in the shell or a bug in the bug database.
Rémi

JSR 292 Goodness: named

Or maybe oracle really is evil? Unlike many people I gave it the benefit of doubt so far but if it starts futzing with the immensely useful bugs database, esp. in such a sneaky way, I'm going to have to reconsider. You are thick with the core Java team at Oracle; maybe you can ask them what's going on?

JSR 292 Goodness: named

I can access to bug 4429536 now.
It's more a db failure than a new policy in my opinion but I will ask.
Rémi

JSR 292 Goodness: named

Indeed that works once again. False alarm (this time, at any rate).

// thanks Rémi

// thanks Rémi

// thanks Rémi

:))