/* Copyright (c) 2007-2013 Timothy Wall, All Rights Reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package com.sun.jna;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import com.sun.jna.win32.DLLCallback;
/** Provides a reference to an association between a native callback closure
* and a Java {@link Callback} closure.
*/
class CallbackReference extends WeakReference {
static final Map callbackMap = new WeakHashMap();
static final Map directCallbackMap = new WeakHashMap();
static final Map pointerCallbackMap = new WeakHashMap();
static final Map allocations = new WeakHashMap();
private static final Map allocatedMemory = Collections.synchronizedMap(new WeakHashMap());
private static final Method PROXY_CALLBACK_METHOD;
static {
try {
PROXY_CALLBACK_METHOD = CallbackProxy.class.getMethod("callback", new Class[] { Object[].class });
}
catch(Exception e) {
throw new Error("Error looking up CallbackProxy.callback() method");
}
}
private static final Map initializers = new WeakHashMap();
static void setCallbackThreadInitializer(Callback cb, CallbackThreadInitializer initializer) {
synchronized(callbackMap) {
if (initializer != null) {
initializers.put(cb, initializer);
}
else {
initializers.remove(cb);
}
}
}
static class AttachOptions extends Structure {
public boolean daemon;
public boolean detach;
public String name;
// Thread name must be UTF8-encoded
{ setStringEncoding("utf8"); }
protected List getFieldOrder() {
return Arrays.asList(new String[] { "daemon", "detach", "name", });
}
}
/** Called from native code to initialize a callback thread. */
private static ThreadGroup initializeThread(Callback cb, AttachOptions args) {
CallbackThreadInitializer init = null;
if (cb instanceof DefaultCallbackProxy) {
cb = ((DefaultCallbackProxy)cb).getCallback();
}
synchronized(callbackMap) {
init = (CallbackThreadInitializer)initializers.get(cb);
}
ThreadGroup group = null;
if (init != null) {
group = init.getThreadGroup(cb);
args.name = init.getName(cb);
args.daemon = init.isDaemon(cb);
args.detach = init.detach(cb);
args.write();
}
return group;
}
/** Return a Callback associated with the given function pointer.
* If the pointer refers to a Java callback trampoline, return the original
* Java Callback. Otherwise, return a proxy to the native function
* pointer.
* @throws IllegalStateException if the given pointer has already been
* mapped to a callback of a different type.
*/
public static Callback getCallback(Class type, Pointer p) {
return getCallback(type, p, false);
}
private static Callback getCallback(Class type, Pointer p, boolean direct) {
if (p == null) {
return null;
}
if (!type.isInterface())
throw new IllegalArgumentException("Callback type must be an interface");
Map map = direct ? directCallbackMap : callbackMap;
synchronized(callbackMap) {
Callback cb = null;
Reference ref = (Reference)pointerCallbackMap.get(p);
if (ref != null) {
cb = (Callback)ref.get();
if (cb != null && !type.isAssignableFrom(cb.getClass())) {
throw new IllegalStateException("Pointer " + p + " already mapped to " + cb);
}
return cb;
}
int ctype = AltCallingConvention.class.isAssignableFrom(type)
? Function.ALT_CONVENTION : Function.C_CONVENTION;
Map foptions = new HashMap(Native.getLibraryOptions(type));
foptions.put(Function.OPTION_INVOKING_METHOD, getCallbackMethod(type));
NativeFunctionHandler h = new NativeFunctionHandler(p, ctype, foptions);
cb = (Callback)Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, h);
// No CallbackReference for this callback
map.put(cb, null);
pointerCallbackMap.put(p, new WeakReference(cb));
return cb;
}
}
Pointer cbstruct;
Pointer trampoline;
// Keep a reference to the proxy to avoid premature GC of it
CallbackProxy proxy;
Method method;
private CallbackReference(Callback callback, int callingConvention, boolean direct) {
super(callback);
TypeMapper mapper = Native.getTypeMapper(callback.getClass());
Class[] nativeParamTypes;
Class returnType;
// Check whether direct mapping may be used, or whether
// we need to fall back to conventional mapping
boolean ppc = Platform.isPPC();
if (direct) {
Method m = getCallbackMethod(callback);
Class[] ptypes = m.getParameterTypes();
for (int i=0;i < ptypes.length;i++) {
// varargs w/FP args via ffi_call fails on ppc (darwin)
if (ppc && (ptypes[i] == float.class
|| ptypes[i] == double.class)) {
direct = false;
break;
}
// No TypeMapper support in native callback code
if (mapper != null
&& mapper.getFromNativeConverter(ptypes[i]) != null) {
direct = false;
break;
}
}
if (mapper != null
&& mapper.getToNativeConverter(m.getReturnType()) != null) {
direct = false;
}
}
String encoding = Native.getStringEncoding(callback.getClass());
if (direct) {
method = getCallbackMethod(callback);
nativeParamTypes = method.getParameterTypes();
returnType = method.getReturnType();
int flags = Native.CB_OPTION_DIRECT;
if (callback instanceof DLLCallback) {
flags |= Native.CB_OPTION_IN_DLL;
}
long peer = Native.createNativeCallback(callback, method,
nativeParamTypes, returnType,
callingConvention, flags,
encoding);
cbstruct = peer != 0 ? new Pointer(peer) : null;
allocatedMemory.put(this, new WeakReference(this));
}
else {
if (callback instanceof CallbackProxy) {
proxy = (CallbackProxy)callback;
}
else {
proxy = new DefaultCallbackProxy(getCallbackMethod(callback), mapper, encoding);
}
nativeParamTypes = proxy.getParameterTypes();
returnType = proxy.getReturnType();
// Generate a list of parameter types that the native code can
// handle. Let the CallbackProxy do any further conversion
// to match the true Java callback method signature
if (mapper != null) {
for (int i=0;i < nativeParamTypes.length;i++) {
FromNativeConverter rc = mapper.getFromNativeConverter(nativeParamTypes[i]);
if (rc != null) {
nativeParamTypes[i] = rc.nativeType();
}
}
ToNativeConverter tn = mapper.getToNativeConverter(returnType);
if (tn != null) {
returnType = tn.nativeType();
}
}
for (int i=0;i < nativeParamTypes.length;i++) {
nativeParamTypes[i] = getNativeType(nativeParamTypes[i]);
if (!isAllowableNativeType(nativeParamTypes[i])) {
String msg = "Callback argument " + nativeParamTypes[i]
+ " requires custom type conversion";
throw new IllegalArgumentException(msg);
}
}
returnType = getNativeType(returnType);
if (!isAllowableNativeType(returnType)) {
String msg = "Callback return type " + returnType
+ " requires custom type conversion";
throw new IllegalArgumentException(msg);
}
int flags = callback instanceof DLLCallback
? Native.CB_OPTION_IN_DLL : 0;
long peer = Native.createNativeCallback(proxy, PROXY_CALLBACK_METHOD,
nativeParamTypes, returnType,
callingConvention, flags,
encoding);
cbstruct = peer != 0 ? new Pointer(peer) : null;
}
}
private Class getNativeType(Class cls) {
if (Structure.class.isAssignableFrom(cls)) {
// Make sure we can instantiate an argument of this type
Structure.validate(cls);
if (!Structure.ByValue.class.isAssignableFrom(cls))
return Pointer.class;
}
else if (NativeMapped.class.isAssignableFrom(cls)) {
return NativeMappedConverter.getInstance(cls).nativeType();
}
else if (cls == String.class
|| cls == WString.class
|| cls == String[].class
|| cls == WString[].class
|| Callback.class.isAssignableFrom(cls)) {
return Pointer.class;
}
return cls;
}
private static Method checkMethod(Method m) {
if (m.getParameterTypes().length > Function.MAX_NARGS) {
String msg = "Method signature exceeds the maximum "
+ "parameter count: " + m;
throw new UnsupportedOperationException(msg);
}
return m;
}
/** Find the first instance of an interface which implements the Callback
* interface or an interface derived from Callback, which defines an
* appropriate callback method.
*/
static Class findCallbackClass(Class type) {
if (!Callback.class.isAssignableFrom(type)) {
throw new IllegalArgumentException(type.getName() + " is not derived from com.sun.jna.Callback");
}
if (type.isInterface()) {
return type;
}
Class[] ifaces = type.getInterfaces();
for (int i=0;i < ifaces.length;i++) {
if (Callback.class.isAssignableFrom(ifaces[i])) {
try {
// Make sure it's got a recognizable callback method
getCallbackMethod(ifaces[i]);
return ifaces[i];
}
catch(IllegalArgumentException e) {
break;
}
}
}
if (Callback.class.isAssignableFrom(type.getSuperclass())) {
return findCallbackClass(type.getSuperclass());
}
return type;
}
private static Method getCallbackMethod(Callback callback) {
return getCallbackMethod(findCallbackClass(callback.getClass()));
}
private static Method getCallbackMethod(Class cls) {
// Look at only public methods defined by the Callback class
Method[] pubMethods = cls.getDeclaredMethods();
Method[] classMethods = cls.getMethods();
Set pmethods = new HashSet(Arrays.asList(pubMethods));
pmethods.retainAll(Arrays.asList(classMethods));
// Remove Object methods disallowed as callback method names
for (Iterator i=pmethods.iterator();i.hasNext();) {
Method m = (Method)i.next();
if (Callback.FORBIDDEN_NAMES.contains(m.getName())) {
i.remove();
}
}
Method[] methods = (Method[])pmethods.toArray(new Method[pmethods.size()]);
if (methods.length == 1) {
return checkMethod(methods[0]);
}
for (int i=0;i < methods.length;i++) {
Method m = methods[i];
if (Callback.METHOD_NAME.equals(m.getName())) {
return checkMethod(m);
}
}
String msg = "Callback must implement a single public method, "
+ "or one public method named '" + Callback.METHOD_NAME + "'";
throw new IllegalArgumentException(msg);
}
/** Set the behavioral options for this callback. */
private void setCallbackOptions(int options) {
cbstruct.setInt(Pointer.SIZE, options);
}
/** Obtain a pointer to the native glue code for this callback. */
public Pointer getTrampoline() {
if (trampoline == null) {
trampoline = cbstruct.getPointer(0);
}
return trampoline;
}
/** Free native resources associated with this callback when GC'd. */
protected void finalize() {
dispose();
}
/** Free native resources associated with this callback. */
protected synchronized void dispose() {
if (cbstruct != null) {
Native.freeNativeCallback(cbstruct.peer);
cbstruct.peer = 0;
cbstruct = null;
allocatedMemory.remove(this);
}
}
/** Dispose of all memory allocated for callbacks. */
static void disposeAll() {
for (Iterator i=allocatedMemory.keySet().iterator();i.hasNext();) {
((Memory)i.next()).dispose();
}
}
private Callback getCallback() {
return (Callback)get();
}
/** If the callback is one we generated to wrap a native function pointer,
return that. Otherwise return null.
*/
private static Pointer getNativeFunctionPointer(Callback cb) {
if (Proxy.isProxyClass(cb.getClass())) {
Object handler = Proxy.getInvocationHandler(cb);
if (handler instanceof NativeFunctionHandler) {
return ((NativeFunctionHandler)handler).getPointer();
}
}
return null;
}
/** Return a {@link Pointer} to the native function address for the
* given callback.
*/
public static Pointer getFunctionPointer(Callback cb) {
return getFunctionPointer(cb, false);
}
/** Native code may call this method with direct=true. */
private static Pointer getFunctionPointer(Callback cb, boolean direct) {
Pointer fp = null;
if (cb == null) {
return null;
}
if ((fp = getNativeFunctionPointer(cb)) != null) {
return fp;
}
int callingConvention = cb instanceof AltCallingConvention
? Function.ALT_CONVENTION : Function.C_CONVENTION;
Map map = direct ? directCallbackMap : callbackMap;
synchronized(callbackMap) {
CallbackReference cbref = (CallbackReference)map.get(cb);
if (cbref == null) {
cbref = new CallbackReference(cb, callingConvention, direct);
map.put(cb, cbref);
pointerCallbackMap.put(cbref.getTrampoline(), new WeakReference(cb));
if (initializers.containsKey(cb)) {
cbref.setCallbackOptions(Native.CB_HAS_INITIALIZER);
}
}
return cbref.getTrampoline();
}
}
private class DefaultCallbackProxy implements CallbackProxy {
private final Method callbackMethod;
private ToNativeConverter toNative;
private final FromNativeConverter[] fromNative;
private final String encoding;
public DefaultCallbackProxy(Method callbackMethod, TypeMapper mapper, String encoding) {
this.callbackMethod = callbackMethod;
this.encoding = encoding;
Class[] argTypes = callbackMethod.getParameterTypes();
Class returnType = callbackMethod.getReturnType();
fromNative = new FromNativeConverter[argTypes.length];
if (NativeMapped.class.isAssignableFrom(returnType)) {
toNative = NativeMappedConverter.getInstance(returnType);
}
else if (mapper != null) {
toNative = mapper.getToNativeConverter(returnType);
}
for (int i=0;i < fromNative.length;i++) {
if (NativeMapped.class.isAssignableFrom(argTypes[i])) {
fromNative[i] = new NativeMappedConverter(argTypes[i]);
}
else if (mapper != null) {
fromNative[i] = mapper.getFromNativeConverter(argTypes[i]);
}
}
if (!callbackMethod.isAccessible()) {
try {
callbackMethod.setAccessible(true);
}
catch(SecurityException e) {
throw new IllegalArgumentException("Callback method is inaccessible, make sure the interface is public: " + callbackMethod);
}
}
}
public Callback getCallback() {
return CallbackReference.this.getCallback();
}
private Object invokeCallback(Object[] args) {
Class[] paramTypes = callbackMethod.getParameterTypes();
Object[] callbackArgs = new Object[args.length];
// convert basic supported types to appropriate Java parameter types
for (int i=0;i < args.length;i++) {
Class type = paramTypes[i];
Object arg = args[i];
if (fromNative[i] != null) {
FromNativeContext context =
new CallbackParameterContext(type, callbackMethod, args, i);
callbackArgs[i] = fromNative[i].fromNative(arg, context);
}
else {
callbackArgs[i] = convertArgument(arg, type);
}
}
Object result = null;
Callback cb = DefaultCallbackProxy.this.getCallback();
if (cb != null) {
try {
result = convertResult(callbackMethod.invoke(cb, callbackArgs));
}
catch (IllegalArgumentException e) {
Native.getCallbackExceptionHandler().uncaughtException(cb, e);
}
catch (IllegalAccessException e) {
Native.getCallbackExceptionHandler().uncaughtException(cb, e);
}
catch (InvocationTargetException e) {
Native.getCallbackExceptionHandler().uncaughtException(cb, e.getTargetException());
}
}
// Synch any structure arguments back to native memory
for (int i=0;i < callbackArgs.length;i++) {
if (callbackArgs[i] instanceof Structure
&& !(callbackArgs[i] instanceof Structure.ByValue)) {
((Structure)callbackArgs[i]).autoWrite();
}
}
return result;
}
/** Called from native code. All arguments are in an array of
* Object as the first argument. Converts all arguments to types
* required by the actual callback method signature, and converts
* the result back into an appropriate native type.
* This method <em>must not</em> throw exceptions.
*/
public Object callback(Object[] args) {
try {
return invokeCallback(args);
}
catch (Throwable t) {
Native.getCallbackExceptionHandler().uncaughtException(getCallback(), t);
return null;
}
}
/** Convert argument from its basic native type to the given
* Java parameter type.
*/
private Object convertArgument(Object value, Class dstType) {
if (value instanceof Pointer) {
if (dstType == String.class) {
value = ((Pointer)value).getString(0, encoding);
}
else if (dstType == WString.class) {
value = new WString(((Pointer)value).getWideString(0));
}
else if (dstType == String[].class) {
value = ((Pointer)value).getStringArray(0, encoding);
}
else if (dstType == WString[].class) {
value = ((Pointer)value).getWideStringArray(0);
}
else if (Callback.class.isAssignableFrom(dstType)) {
value = CallbackReference.this.getCallback(dstType, (Pointer)value);
}
else if (Structure.class.isAssignableFrom(dstType)) {
// If passed by value, don't hold onto the pointer, which
// is only valid for the duration of the callback call
if (Structure.ByValue.class.isAssignableFrom(dstType)) {
Structure s = Structure.newInstance(dstType);
byte[] buf = new byte[s.size()];
((Pointer)value).read(0, buf, 0, buf.length);
s.getPointer().write(0, buf, 0, buf.length);
s.read();
value = s;
}
else {
Structure s = Structure.newInstance(dstType, (Pointer)value);
s.conditionalAutoRead();
value = s;
}
}
}
else if ((boolean.class == dstType || Boolean.class == dstType)
&& value instanceof Number) {
value = Function.valueOf(((Number)value).intValue() != 0);
}
return value;
}
private Object convertResult(Object value) {
if (toNative != null) {
value = toNative.toNative(value, new CallbackResultContext(callbackMethod));
}
if (value == null)
return null;
Class cls = value.getClass();
if (Structure.class.isAssignableFrom(cls)) {
if (Structure.ByValue.class.isAssignableFrom(cls)) {
return value;
}
return ((Structure)value).getPointer();
}
else if (cls == boolean.class || cls == Boolean.class) {
return Boolean.TRUE.equals(value) ?
Function.INTEGER_TRUE : Function.INTEGER_FALSE;
}
else if (cls == String.class || cls == WString.class) {
return getNativeString(value, cls == WString.class);
}
else if (cls == String[].class || cls == WString.class) {
StringArray sa = cls == String[].class
? new StringArray((String[])value, encoding)
: new StringArray((WString[])value);
// Delay GC until array itself is GC'd.
allocations.put(value, sa);
return sa;
}
else if (Callback.class.isAssignableFrom(cls)) {
return getFunctionPointer((Callback)value);
}
return value;
}
public Class[] getParameterTypes() {
return callbackMethod.getParameterTypes();
}
public Class getReturnType() {
return callbackMethod.getReturnType();
}
}
/** Provide invocation handling for an auto-generated Java interface proxy
* for a native function pointer.
* Cf. Library.Handler
*/
private static class NativeFunctionHandler implements InvocationHandler {
private final Function function;
private final Map options;
public NativeFunctionHandler(Pointer address, int callingConvention, Map options) {
String encoding = (String)options.get(Library.OPTION_STRING_ENCODING);
this.function = new Function(address, callingConvention, encoding);
this.options = options;
}
/** Chain invocation to the native function. */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Library.Handler.OBJECT_TOSTRING.equals(method)) {
String str = "Proxy interface to " + function;
Method m = (Method)options.get(Function.OPTION_INVOKING_METHOD);
Class cls = findCallbackClass(m.getDeclaringClass());
str += " (" + cls.getName() + ")";
return str;
}
else if (Library.Handler.OBJECT_HASHCODE.equals(method)) {
return new Integer(hashCode());
}
else if (Library.Handler.OBJECT_EQUALS.equals(method)) {
Object o = args[0];
if (o != null && Proxy.isProxyClass(o.getClass())) {
return Function.valueOf(Proxy.getInvocationHandler(o) == this);
}
return Boolean.FALSE;
}
if (Function.isVarArgs(method)) {
args = Function.concatenateVarArgs(args);
}
return function.invoke(method.getReturnType(), args, options);
}
public Pointer getPointer() {
return function;
}
}
/** Returns whether the given class is supported in native code.
* Other types (String, WString, Structure, arrays, NativeMapped,
* etc) are supported in the Java library.
*/
private static boolean isAllowableNativeType(Class cls) {
return cls == void.class || cls == Void.class
|| cls == boolean.class || cls == Boolean.class
|| cls == byte.class || cls == Byte.class
|| cls == short.class || cls == Short.class
|| cls == char.class || cls == Character.class
|| cls == int.class || cls == Integer.class
|| cls == long.class || cls == Long.class
|| cls == float.class || cls == Float.class
|| cls == double.class || cls == Double.class
|| (Structure.ByValue.class.isAssignableFrom(cls)
&& Structure.class.isAssignableFrom(cls))
|| Pointer.class.isAssignableFrom(cls);
}
private static Pointer getNativeString(Object value, boolean wide) {
if (value != null) {
NativeString ns = new NativeString(value.toString(), wide);
// Delay GC until string itself is GC'd.
allocations.put(value, ns);
return ns.getPointer();
}
return null;
}
}