Skip to content

Commit

Permalink
Use handles to dispath default methods
Browse files Browse the repository at this point in the history
Default methods on interfaces fed to java.lang.Proxy cannot be
invoked without using method handles, which requires a Lookup with
private access (acquired within the interface class). This commit
adds a load path that propagates such a Lookup through to the
eventual Proxy instance, allowing it to skip binding default
methods to their (nonexistent) native function, instead calling
the provided default method body.

Fixes jnr#249.
  • Loading branch information
headius committed Jun 14, 2021
1 parent 519a5b1 commit 78f5cf3
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 14 deletions.
26 changes: 23 additions & 3 deletions src/main/java/jnr/ffi/LibraryLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
Expand Down Expand Up @@ -75,9 +76,9 @@ public abstract class LibraryLoader<T> {
private final TypeMapper.Builder typeMapperBuilder = new TypeMapper.Builder();
private final FunctionMapper.Builder functionMapperBuilder = new FunctionMapper.Builder();
private final Class<T> interfaceClass;
private final MethodHandles.Lookup lookup;
private boolean failImmediately = false;


/**
* Creates a new {@code LibraryLoader} instance.
*
Expand All @@ -89,8 +90,26 @@ public static <T> LibraryLoader<T> create(Class<T> interfaceClass) {
return FFIProvider.getSystemProvider().createLibraryLoader(interfaceClass);
}

/**
* Creates a new {@code LibraryLoader} instance.
*
* @param <T> The library type.
* @param interfaceClass the interface that describes the native library functions
* @param lookup the {@link java.lang.invoke.MethodHandles.Lookup} to use for invoking default functions
* @return A {@code LibraryLoader} instance.
*/
public static <T> LibraryLoader<T> create(Class<T> interfaceClass, MethodHandles.Lookup lookup) {
return FFIProvider.getSystemProvider().createLibraryLoader(interfaceClass, lookup);
}

protected LibraryLoader(Class<T> interfaceClass) {
this.interfaceClass = interfaceClass;
this.lookup = null;
}

protected LibraryLoader(Class<T> interfaceClass, MethodHandles.Lookup lookup) {
this.interfaceClass = interfaceClass;
this.lookup = lookup;
}

/**
Expand Down Expand Up @@ -415,7 +434,7 @@ public T load() {
optionMap.put(LibraryOption.FunctionMapper, functionMappers.size() > 1 ? new CompositeFunctionMapper(functionMappers) : functionMappers.get(0));

try {
return loadLibrary(interfaceClass, Collections.unmodifiableList(libraryNames), getSearchPaths(),
return loadLibrary(interfaceClass, lookup, Collections.unmodifiableList(libraryNames), getSearchPaths(),
Collections.unmodifiableMap(optionMap), failImmediately);

} catch (LinkageError error) {
Expand Down Expand Up @@ -453,13 +472,14 @@ private Collection<String> getSearchPaths() {
* Implemented by FFI providers to load the actual library.
*
* @param interfaceClass The java class that describes the functions to be mapped.
* @param lookup The lookup to use for invoking default methods, if necessary
* @param libraryNames A list of libraries to load and search for symbols.
* @param searchPaths The paths to search for libraries to be loaded.
* @param options The options to apply when loading the library.
* @param failImmediately whether to fast-fail when the library does not implement the requested functions
* @return an instance of {@code interfaceClass} that will call the native methods.
*/
protected abstract T loadLibrary(Class<T> interfaceClass, Collection<String> libraryNames,
protected abstract T loadLibrary(Class<T> interfaceClass, MethodHandles.Lookup lookup, Collection<String> libraryNames,
Collection<String> searchPaths, Map<LibraryOption, Object> options,
boolean failImmediately);

Expand Down
12 changes: 12 additions & 0 deletions src/main/java/jnr/ffi/provider/FFIProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import jnr.ffi.LibraryLoader;

import java.lang.invoke.MethodHandles;

/**
* This class defines the facilities a JNR FFI provider must provide.
*
Expand Down Expand Up @@ -53,6 +55,16 @@ protected FFIProvider() {}
*/
public abstract <T> LibraryLoader<T> createLibraryLoader(Class<T> interfaceClass);

/**
* Creates a new {@link LibraryLoader} instance.
*
* @param <T> The library type.
* @param interfaceClass The library interface class.
* @param lookup the {@link java.lang.invoke.MethodHandles.Lookup} to use for invoking default functions
* @return the {@code LibraryLoader} instance.
*/
public abstract <T> LibraryLoader<T> createLibraryLoader(Class<T> interfaceClass, MethodHandles.Lookup lookup);

private static final class SystemProviderSingletonHolder {
private static final FFIProvider INSTANCE = getInstance();

Expand Down
8 changes: 7 additions & 1 deletion src/main/java/jnr/ffi/provider/InvalidProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import jnr.ffi.LibraryOption;
import jnr.ffi.Runtime;

import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Map;

Expand All @@ -45,11 +46,16 @@ public Runtime getRuntime() {
public <T> LibraryLoader<T> createLibraryLoader(Class<T> interfaceClass) {
return new LibraryLoader<T>(interfaceClass) {
@Override
protected T loadLibrary(Class<T> interfaceClass, Collection<String> libraryNames, Collection<String> searchPaths, Map<LibraryOption, Object> options, boolean failImmediately) {
protected T loadLibrary(Class<T> interfaceClass, MethodHandles.Lookup lookup, Collection<String> libraryNames, Collection<String> searchPaths, Map<LibraryOption, Object> options, boolean failImmediately) {
UnsatisfiedLinkError error = new UnsatisfiedLinkError(message);
error.initCause(cause);
throw error;
}
};
}

@Override
public <T> LibraryLoader<T> createLibraryLoader(Class<T> interfaceClass, MethodHandles.Lookup lookup) {
return createLibraryLoader(interfaceClass);
}
}
19 changes: 18 additions & 1 deletion src/main/java/jnr/ffi/provider/NativeInvocationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

package jnr.ffi.provider;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collections;
Expand All @@ -30,19 +32,34 @@
public class NativeInvocationHandler implements InvocationHandler {
private volatile Map<Method, Invoker> fastLookupTable;
private final Map<Method, Invoker> invokerMap;
private final MethodHandles.Lookup lookup;

/**
* Creates a new InvocationHandler instance.
*
* @param invokers A map of method invokers
*
*/
public NativeInvocationHandler(Map<Method, Invoker> invokers) {
public NativeInvocationHandler(Map<Method, Invoker> invokers, MethodHandles.Lookup lookup) {
this.invokerMap = invokers;
this.fastLookupTable = Collections.emptyMap();
this.lookup = lookup;
}

public Object invoke(Object self, Method method, Object[] argArray) throws Throwable {
// skip default methods
if (method.isDefault()) {
if (lookup == null) {
throw new AbstractMethodError("default methods not supported without Lookup or code generation");
}

MethodHandle handle = lookup.unreflectSpecial(method, method.getDeclaringClass());
Object[] newArgs = new Object[argArray.length + 1];
System.arraycopy(argArray, 0, newArgs, 1, argArray.length);
newArgs[0] = self;
return handle.invokeWithArguments(newArgs);
}

Invoker invoker = fastLookupTable.get(method);
return invoker != null ? invoker.invoke(self, argArray) : lookupAndCacheInvoker(method).invoke(self, argArray);
}
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/jnr/ffi/provider/jffi/AsmLibraryLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.objectweb.asm.ClassWriter;

import java.io.PrintWriter;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
Expand Down Expand Up @@ -72,8 +73,13 @@ public class AsmLibraryLoader extends LibraryLoader {
private final NativeRuntime runtime = NativeRuntime.getInstance();

@Override
<T> T loadLibrary(NativeLibrary library, Class<T> interfaceClass, Map<LibraryOption, ?> libraryOptions,
boolean failImmediately /* ignored, asm loader eagerly binds everything */) {
<T> T loadLibrary(
NativeLibrary library,
Class<T> interfaceClass,
MethodHandles.Lookup lookup /* ignored, asm loader avoids binding default methods */,
Map<LibraryOption, ?> libraryOptions,
boolean failImmediately /* ignored, asm loader eagerly binds everything */) {

AsmClassLoader oldClassLoader = classLoader.get();

// Only create a new class loader if this was not a recursive call (i.e. loading a library as a result of loading another library)
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/jnr/ffi/provider/jffi/LibraryLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import jnr.ffi.mapper.TypeMapper;
import jnr.ffi.provider.NullTypeMapper;

import java.lang.invoke.MethodHandles;
import java.util.Map;

public abstract class LibraryLoader {
Expand Down Expand Up @@ -60,5 +61,5 @@ static CompositeTypeMapper newClosureTypeMapper(AsmClassLoader classLoader, Sign
new CachingTypeMapper(new AnnotationTypeMapper()));
}

abstract <T> T loadLibrary(NativeLibrary library, Class<T> interfaceClass, Map<LibraryOption, ?> libraryOptions, boolean failImmediately);
abstract <T> T loadLibrary(NativeLibrary library, Class<T> interfaceClass, MethodHandles.Lookup lookup, Map<LibraryOption, ?> libraryOptions, boolean failImmediately);
}
13 changes: 9 additions & 4 deletions src/main/java/jnr/ffi/provider/jffi/NativeLibraryLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import jnr.ffi.LibraryOption;

import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Map;

Expand All @@ -35,14 +36,18 @@ class NativeLibraryLoader<T> extends jnr.ffi.LibraryLoader<T> {
super(interfaceClass);
}

public T loadLibrary(Class<T> interfaceClass, Collection<String> libraryNames, Collection<String> searchPaths,
Map<LibraryOption, Object> options, boolean failImmediately) {
NativeLibraryLoader(Class<T> interfaceClass, MethodHandles.Lookup lookup) {
super(interfaceClass, lookup);
}

public T loadLibrary(Class<T> interfaceClass, MethodHandles.Lookup lookup, Collection<String> libraryNames, Collection<String> searchPaths,
Map<LibraryOption, Object> options, boolean failImmediately) {
NativeLibrary nativeLibrary = new NativeLibrary(libraryNames, searchPaths);

try {
return ASM_ENABLED
? new AsmLibraryLoader().loadLibrary(nativeLibrary, interfaceClass, options, failImmediately)
: new ReflectionLibraryLoader().loadLibrary(nativeLibrary, interfaceClass, options, failImmediately);
? new AsmLibraryLoader().loadLibrary(nativeLibrary, interfaceClass, lookup, options, failImmediately)
: new ReflectionLibraryLoader().loadLibrary(nativeLibrary, interfaceClass, lookup, options, failImmediately);

} catch (RuntimeException ex) {
throw ex;
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/jnr/ffi/provider/jffi/Provider.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import jnr.ffi.Runtime;
import jnr.ffi.provider.FFIProvider;

import java.lang.invoke.MethodHandles;


public final class Provider extends FFIProvider {
private final NativeRuntime runtime;
Expand All @@ -36,4 +38,8 @@ public final Runtime getRuntime() {
public <T> jnr.ffi.LibraryLoader<T> createLibraryLoader(Class<T> interfaceClass) {
return new NativeLibraryLoader<T>(interfaceClass);
}

public <T> jnr.ffi.LibraryLoader<T> createLibraryLoader(Class<T> interfaceClass, MethodHandles.Lookup lookup) {
return new NativeLibraryLoader<T>(interfaceClass, lookup);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import jnr.ffi.provider.NativeVariable;

import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.AbstractMap;
Expand All @@ -51,7 +52,7 @@
class ReflectionLibraryLoader extends LibraryLoader {

@Override
<T> T loadLibrary(NativeLibrary library, Class<T> interfaceClass, Map<LibraryOption, ?> libraryOptions, boolean failImmediately) {
<T> T loadLibrary(NativeLibrary library, Class<T> interfaceClass, MethodHandles.Lookup lookup, Map<LibraryOption, ?> libraryOptions, boolean failImmediately) {
Map<Method, Invoker> invokers = new LazyLoader<T>(library, interfaceClass, libraryOptions);

if (failImmediately) {
Expand All @@ -61,6 +62,7 @@ <T> T loadLibrary(NativeLibrary library, Class<T> interfaceClass, Map<LibraryOpt

// force all functions to bind
for (NativeFunction function : scanner.functions()) {
if (function.getMethod().isDefault()) continue;
invokers.get(function.getMethod());
}

Expand All @@ -71,7 +73,7 @@ <T> T loadLibrary(NativeLibrary library, Class<T> interfaceClass, Map<LibraryOpt
}

return interfaceClass.cast(Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class[]{ interfaceClass, LoadedLibrary.class }, new NativeInvocationHandler(invokers)));
new Class[]{ interfaceClass, LoadedLibrary.class }, new NativeInvocationHandler(invokers, lookup)));
}

private static final class FunctionNotFoundInvoker implements Invoker {
Expand Down

0 comments on commit 78f5cf3

Please sign in to comment.