java-native-access/jna · error · java.lang.IllegalArgumentException

Library must be a proxy class

Error message

Library must be a proxy class

What it means

Native.synchronizedLibrary wraps a Library proxy so all calls are serialized, but it only works on the dynamic proxy instances returned by Native.loadLibrary/Native.load. Passing any other Library implementation (hand-written class, different proxy) fails Proxy.isProxyClass and throws this IllegalArgumentException.

Source

Thrown at src/com/sun/jna/Native.java:1306

    /** Set the OS last error code.  The value will be saved on a per-thread
     * basis.
     */
    public static native void setLastError(int code);

    /**
     * Returns a synchronized (thread-safe) library backed by the specified
     * library.  This wrapping will prevent simultaneous invocations of any
     * functions mapped to a given {@link NativeLibrary}.  Note that the
     * native library may still be sensitive to being called from different
     * threads.
     * <p>
     * @param  library the library to be "wrapped" in a synchronized library.
     * @return a synchronized view of the specified library.
     */
    public static Library synchronizedLibrary(final Library library) {
        Class<?> cls = library.getClass();
        if (!Proxy.isProxyClass(cls)) {
            throw new IllegalArgumentException("Library must be a proxy class");
        }
        InvocationHandler ih = Proxy.getInvocationHandler(library);
        if (!(ih instanceof Library.Handler)) {
            throw new IllegalArgumentException("Unrecognized proxy handler: " + ih);
        }
        final Library.Handler handler = (Library.Handler)ih;
        InvocationHandler newHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                synchronized(handler.getNativeLibrary()) {
                    return handler.invoke(library, method, args);
                }
            }
        };
        return (Library)Proxy.newProxyInstance(cls.getClassLoader(),
                                               cls.getInterfaces(),
                                               newHandler);
    }

View on GitHub (pinned to d036ad9781)

Solutions

  1. Pass the exact object returned by Native.loadLibrary/Native.load (the JNA proxy) to synchronizedLibrary
  2. If you need a hand-written implementation, add your own synchronization instead of using synchronizedLibrary
  3. Create the library via Native.load(..., Library.class) first, then wrap: Native.synchronizedLibrary(lib)
  4. Check for wrapper layers (caching frameworks) that may have replaced the proxy with another object

Example fix

// before
MyLib impl = new MyLibImpl(); // hand-written
Library sync = Native.synchronizedLibrary(impl);
// after
MyLib lib = Native.load("mylib", MyLib.class);
MyLib sync = (MyLib) Native.synchronizedLibrary(lib);
Defensive patterns

Strategy: type-guard

Validate before calling

static Library synchronizeIfProxy(Library lib) {
    if (!Proxy.isProxyClass(lib.getClass())) {
        throw new IllegalArgumentException("Pass the proxy returned by Native.load, not " + lib.getClass());
    }
    return (Library) Native.synchronizedLibrary(lib);
}

Type guard

boolean isJnaLibraryProxy(Library lib) {
    return Proxy.isProxyClass(lib.getClass())
        && Proxy.getInvocationHandler(lib) instanceof com.sun.jna.Library.Handler;
}

Try / catch

try { return Native.synchronizedLibrary(lib); } catch (IllegalArgumentException e) { if (String.valueOf(e.getMessage()).equals("Library must be a proxy class")) { Library real = Native.load(libName, lib.getClass().asSubclass(Library.class)); return Native.synchronizedLibrary(real); } throw e; }

Prevention

When it happens

Trigger: Calling Native.synchronizedLibrary(obj) where obj is a hand-rolled implementation of a Library interface, a subclass, a CGLIB/other proxy, or a library instance obtained from a different framework.

Common situations: Wrapping a mocked Library in tests; passing a manually implemented interface to add thread-safety; mixing libraries created by another wrapper utility; assuming synchronizedLibrary accepts any Library.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of java-native-access/jna@d036ad9781 (2026-09-12). Data as JSON: /api/errors/61cc35c6b3ee0f97. Report an issue: GitHub.