LibraryManager.java
/*
** Module : LibraryManager.java
** Abstract : client side implementation for invocation of shared library functions
**
** Copyright (c) 2013-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20140109 First version.
** 002 GES 20140212 Removed debugging code. Added flag to tell native library loading code that
** the library is being loaded persistently. This is necessary for some native
** platforms, where different flags are used.
** 003 CA 20181029 CALL handle's DLL-CALL-TYPE has a quirk where the native API is not invoked,
** just resolved - added the Signature.noInvoke to help with this.
** 005 CA 20220515 Allow library and memptr calls to be executed on server-side.
** 006 GBB 20240826 Changing methods access modifiers after moving LibraryDaemon out of the package.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.library;
import java.util.*;
import com.goldencode.p2j.util.*;
/**
* Client side implementation for invocation of shared library functions. This is the Java code
* that drives the library loading/unloading, function address lookup, marshalling/
* unmarshalling of parameters and the invocation of the function. Much of these features will
* ultimately resolve down to native method calls in our backing JNI library.
*/
public class LibraryManager
{
/** The type of the argument or return value is a signed 8-bit value. */
public static final int TYPE_SIGNED_INT8 = 1;
/** The type of the argument or return value is an unsigned 8-bit value. */
public static final int TYPE_UNSIGNED_INT8 = 2;
/** The type of the argument or return value is a signed 16-bit value. */
public static final int TYPE_SIGNED_INT16 = 3;
/** The type of the argument or return value is an unsigned 16-bit value. */
public static final int TYPE_UNSIGNED_INT16 = 4;
/** The type of the argument or return value is a signed 32-bit value. */
public static final int TYPE_SIGNED_INT32 = 5;
/** The type of the argument or return value is an unsigned 32-bit value. */
public static final int TYPE_UNSIGNED_INT32 = 6;
/** The type of the argument or return value is a signed 64-bit value. */
public static final int TYPE_SIGNED_INT64 = 7;
/** The type of the argument or return value is a 32-bit floating point value. */
public static final int TYPE_FLOAT = 8;
/** The type of the argument or return value is a 64-bit floating point value. */
public static final int TYPE_DOUBLE = 9;
/** The type of the argument or return value is a memory address (a pointer). */
public static final int TYPE_POINTER = 10;
/** The type of the argument or return value is void. */
public static final int TYPE_VOID = 11;
/** Client is running the UNIX/Linux platform. */
public static final int PLATFORM_UNIX = 1;
/** Client is running the Windows platform. */
public static final int PLATFORM_WINDOWS = 2;
/** No error occurred. */
public static final int ERROR_NONE = 0;
/** Invalid calling convention. */
public static final int ERROR_INVALID_CALL_CONV = -1;
/** Invalid data type specified. */
public static final int ERROR_INVALID_TYPE = -2;
/** Invalid data type specified. */
public static final int ERROR_UNKNOWN_FAILURE = -3;
/** Out of memory error occurred. */
public static final int ERROR_OUT_OF_MEMORY = -4;
/** An error occurred when trying to load the library. */
public static final int ERROR_LIBRARY_LOAD = -5;
/** An error occurred when trying to find the function address. */
public static final int ERROR_MISSING_ENTRYPOINT = -6;
/** Use the default calling convention for the platform. */
private static final int CONVENTION_DEFAULT = 1;
/** Use the STDCALL calling convention (only valid on 32-bit Windows). */
private static final int CONVENTION_STDCALL = 2;
/** Use the CDECL calling convention (only valid on 32-bit Windows). */
private static final int CONVENTION_CDECL = 3;
/** Use the PASCAL calling convention (only valid on 32-bit Windows). */
private static final int CONVENTION_PASCAL = 4;
/** Caches the module handles of currently loaded libraries. */
private static final Map<String, Long> libcache =
Collections.synchronizedMap(new HashMap<String, Long>());
/** Caches the memory addresses of functions in currently loaded libraries. */
private static final Map<Long, Map<String, Long>> addrcache =
Collections.synchronizedMap(new HashMap<Long, Map<String, Long>>());
/** Size of a native memory pointer in bytes. */
private static int ptrSize = -1;
/** The platform on which the client is running. */
private static int platform = 0;
/**
* Private constructor so instances are not created.
*/
private LibraryManager()
{
}
/**
* Load the native library containing functions for library management. Only call this
* on the client-side!
*/
public static void init()
{
System.loadLibrary("p2j");
ptrSize = wordSize();
platform = platform();
}
/**
* Invoke the defined native API call, loading (and optionally unloading) the library as
* needed.
* <p>
* All exceptions will be raised on the server-side (not here). Instead of throwing
* an exception, this code will set an error code and return.
*
* @param libname
* The library name where this native procedure should be found.
* @param funcname
* The function name being called in the library (except where an ordinal is being
* used).
* @param ordinal
* The entry point's ordinal or -1 if the entry point should be found by name.
* @param persistent
* <code>true</code> to leave the library loaded when the native call is complete.
* @param conv
* The calling convention to be used in this native call.
* @param signature
* Contains the return value and argument descriptors. In the case of the arguments
* there will be 1 descriptor for each argument to the call (where the 0 index is
* the leftmost argument and the numargs - 1 index is the rightmost). Each
* descriptor defines the type and will contain any input value in the case of an
* argument. If the return value descriptor is <code>null</code>, the call should
* be treated as a void return.
*
* @return The same signature instance will be updated with output (and/or return data)
* upon a successful call or with an error code on any failure.
*/
public static Signature invoke(String libname,
String funcname,
int ordinal,
boolean persistent,
CallingConvention conv,
Signature signature)
{
// save off our platform type so the server can handle error processing appropriately
// if a failure occurs later
signature.setPlatform(platform);
// load the library if it is not already loaded
long mod = obtainLibrary(libname, persistent);
if (mod == 0)
{
signature.setErrorCode(ERROR_LIBRARY_LOAD);
return signature;
}
int argsize = (ptrSize * signature.getArgumentSize());
// find function address
long addr = obtainFunctionAddress(mod,
(conv == CallingConvention.STDCALL),
funcname,
ordinal,
argsize,
persistent);
if (addr == 0)
{
signature.setErrorCode(ERROR_MISSING_ENTRYPOINT);
return signature;
}
if (signature.isNoInvoke())
{
return signature;
}
// determine the calling convention
int callconv = calcCallingConv(conv);
// return value prep
BaseNativeType retval = signature.getReturnValue();
int rettype = (retval == null) ? TYPE_VOID : retval.type();
long retaddr = 0;
memptr retptr = null;
if (retval != null)
{
// return values are not handled as pointers to pointers (void**), but just as a single
// level of indirection (a pointer to a block of memory large enough to hold the
// result); the core render() processing for BNT does not work for return types, so
// we manually bypass that; no rendering is needed, we just need to allocate the memory
retptr = new memptr();
int sz = retval.isPassByPointer() ? ptrSize : retval.size();
retptr.setLength(sz);
retaddr = retptr.getPointerValue().longValue();
}
// prepare and render native arguments
int numargs = signature.getArgumentSize();
int[] argtypes = new int[numargs];
long values = 0;
memptr argvals = null;
if (numargs > 0)
{
// the parameter values are each rendered into their own memory buffer and the address
// is placed into an "array" of pointers, one per argument from left (index 0) to right
// (index numargs - 1); the argvals buffer is used to "manually" setup this C-language
// style array
argvals = new memptr();
argvals.setLength(argsize);
for (int i = 0; i < numargs; i++)
{
BaseNativeType bnt = signature.getArgument(i);
argtypes[i] = bnt.type();
long argaddr = bnt.render();
// memptr has a 1-based index
int idx = (i * ptrSize) + 1;
if (ptrSize == 4)
{
argvals.setLong(argaddr, idx);
}
else
{
argvals.setInt64(argaddr, idx);
}
}
values = argvals.getPointerValue().longValue();
}
// dispatch the call
int rc = dispatch(addr, callconv, rettype, retaddr, numargs, argtypes, values);
// check for errors
if (rc != ERROR_NONE)
{
signature.setErrorCode(rc);
return signature;
}
// copy back return value
if (retval != null)
{
retval.restore(retaddr);
}
// copy back output arguments
for (int k = 0; k < numargs; k++)
{
signature.getArgument(k).restore();
}
// clean up
if (argvals != null)
{
argvals.setLength(0);
}
// unload if needed (per-call unload only occurs when we are non-persistent and the
// library was not currently loaded at the time of this call)
if (!persistent && !libcache.containsKey(libname))
unload(mod);
return signature;
}
/**
* Attempts to unload the library identified by the given name. In the 4GL, there is no
* checking of library names on load or unload. This combined with operating system search
* processing (which varies by platform), means that the input to this method must be
* processed without any real intelligence. If the given library name corresponds exactly
* with an input used previously to load a library persistently (AND that library has never
* been unloaded), then a call to the native OS library unloading mechanism will be made for
* the module handle that was saved off for the given library name. That caching of the
* module handle occurs at load-time (which is an implicit part of function invocation) when
* the library is loaded persistently. Non-persistent loads are immediately unloaded and
* don't get affected by this code. This code will never raise an error or otherwise give
* any indication of success or failure, just like the behavior in the 4GL. The single 4GL
* error processing behavior is implemented on the server-side instead of here. This means
* that any kind of input string (including <code>null</code>) can be provided and the lack
* of a corresponding cached library will be silently ignored. The call to the OS will only
* occur when there is a cached module handle.
*
* @param libname
* The name of the library to attempt to unload.
*/
public static void release(String libname)
{
Long mod = libcache.remove(libname);
if (mod != null)
{
// clear our function address cache
addrcache.remove(mod);
// try the OS unloading facility
unload(mod);
}
}
/**
* Reports on the number of bytes needed to store a native memory pointer.
*
* @return This will return 4 on 32-bit systems and 8 for 64-bit platforms.
*/
static int ptrSize()
{
return ptrSize;
}
/**
* Reports on the number of bytes that the native stack uses for push/pop operations (the
* width or size of the word used).
*
* @return This will return 4 on 32-bit systems and 8 for 64-bit platforms.
*/
private static native int wordSize();
/**
* Reports the platform type on which the client is running.
*
* @return A PLATFORM_* constant.
*/
private static native int platform();
/**
* Attempts to load the named library. This will use the operating-system runtime library
* loading mechanism.
*
* @param libname
* This may be any valid library filename for the current system, including names
* that contain path separators (both relative and absolute). This should not be
* <code>null</code> or the empty string.
* @param persist
* <code>true</code> if the library is being loaded persistently. This allows the
* native code a chance to differentially process if that platform requires it.
*
* @return An operating-system specific "handle" to the library. Under Windows this is
* the "module handle" of type HMODULE and under Linux it is just referred to as
* a "handle" that is returned as a void pointer. In either case, if
* <code>NULL</code> (0) is returned, then the call failed.
*/
private static native long load(String libname, boolean persist);
/**
* Attempts to unload the library identified by the "handle" which was returned from a
* previous call to {@link #load}. This will use the operating-system runtime library
* unloading mechanism. Although this call may fail, Progress seems to always silently ignore
* any failures, so this method has no return value.
*
* @param modhandle
* This must be a valid handle to the library.
*/
private static native void unload(long modhandle);
/**
* Uses the operating-system runtime library lookup facility to obtain the address of the
* given symbol in the library. The symbol name must match exactly, including the case
* and any decorations that exist as exported in the library.
*
* @param modhandle
* This must be a valid handle to a library that is already loaded.
* @param funcname
* This is the exact symbol to lookup in the library.
*
* @return The memory address of the function or <code>NULL</code> (0) if the function
* cannot be found by that name in the given library.
*/
private static native long findByName(long modhandle, String funcname);
/**
* Uses the operating-system runtime library lookup facility to obtain the address of the
* given ordinal in the library. Only some operating systems allow exporting functions by
* ordinal number. Those that do allow it, will typically have a table of contiguous ordinal
* values in the library, each of which represents a function that may or may not be also
* exported by name (as a symbol).
*
* @param modhandle
* This must be a valid handle to a library that is already loaded.
* @param ordinal
* The ordinal to lookup in the library.
*
* @return The memory address of the function or <code>NULL</code> (0) if the function cannot
* be found by that ordinal in the given library. Operating systems that do not
* support ordinal exports will always return 0.
*/
private static native long findByOrdinal(long modhandle, int ordinal);
/**
* Call the function with the specified arguments and return value processing.
*
* @param funcAddr
* The memory address of the function to call. Must not be <code>NULL</code>.
* @param callConv
* A constant representing the calling convention to be used. This is only honored on
* 32-bit Windows, where there are multiple calling conventions to choose from.
* @param retType
* Specifies the data type of the return value. Ignored if the <code>retAddr</code> is
* <code>NULL</code>.
* @param retAddr
* The memory address in which to place the return value. The call is assumed to have
* no return value (or a return value that can be ignored) if this pointer is
* <code>NULL</code>.
* @param numArgs
* The number of arguments in the called function's signature. Must be non-negative
* (0 is valid).
* @param argTypes
* If <code>numArgs</code> is greater than 0, this is an array that specifies the data
* type of each argment from left to right (the 0 index of the array corresponds to the
* leftmost argument).
* @param argAddr
* If <code>numArgs</code> is greater than 0, this must be the memory address of the
* array of <code>void*</code>. Each one points to an argment value from left to right
* (the 0 index of the array corresponds to the leftmost argument).
*
* @return A code indicating success or failure of the dispatching.
*/
private static native int dispatch(long funcAddr,
int callConv,
int retType,
long retAddr,
int numArgs,
int[] argTypes,
long argAddr);
/**
* Obtain the library handle for the given library name, loading the library if needed. If
* a library with exactly the same (even in case and whitespace) name is already loaded, the
* cached handle will be returned. If loading is requested with persistence and the library
* was not already loaded, then any newly loaded library will have its module handle cached.
*
* @param libname
* The library name to be loaded. In general, no modifications to this string will
* be done (the exception is in the native layer for Windows loading, where forward
* slashes are converted to backslashes).
* @param persistent
* If <code>true</code>, the library should not be unloaded at the end of the
* function invocation, which means that this method should cache the module
* handle if it is successfully loaded.
*
* @return An operating-system specific "handle" to the library. Under Windows this is
* the "module handle" of type HMODULE and under Linux it is just referred to as
* a "handle" that is returned as a void pointer. In either case, if
* <code>NULL</code> (0) is returned, then the loading failed.
*/
private static long obtainLibrary(String libname, boolean persistent)
{
// check if the library is already loaded
Long prevmod = libcache.get(libname);
// if found, return
if (prevmod != null)
return prevmod;
// at this point we know we need to try to load the module
long modhandle = load(libname, persistent);
// cache it only if the library is not already cached (at this point we already know that
// prevmod == null) AND we are persistent AND the module was loaded successfully
if (persistent && modhandle != 0)
{
libcache.put(libname, modhandle);
}
return modhandle;
}
/**
* Obtain the function address for the given function name or ordinal. If a function address
* in a loaded library with exactly the same (even in case and whitespace) function name
* has already been found, it will be returned. If the search is requested with persistence,
* then any newly found function addresses will have will be cached.
*
* @param module
* An operating-system specific "handle" to the library. Under Windows this is
* the "module handle" of type HMODULE and under Linux it is just referred to as
* a "handle" that is returned as a void pointer.
* @param stdcall
* <code>true</code> if the STDCALL calling convention is being used for this
* function.
* @param funcname
* The function name to be loaded. In general, no modifications to this string will
* be done (the exception is in the native layer for Windows loading, where STDCALL
* names are checked with an underscore prefix and an at-sign + decimal argument
* length IF the non-decorated version fails).
* @param ordinal
* The ordinal that the API is exported as (on Windows this is used in preference
* to the name) or -1 if no ordinal was specified.
* @param argsize
* The number of bytes used to pass the arguments. This is only used on Windows for
* decorating STDCALL name lookups.
* @param persistent
* If <code>true</code>, the library will not be unloaded at the end of the
* function invocation, which means that this method should cache the function
* address if it is successfully found.
*
* @return An system specific memory address (pointer) of the function. If
* <code>NULL</code> (0) is returned, then the lookup failed.
*/
private static long obtainFunctionAddress(long module,
boolean stdcall,
String funcname,
int ordinal,
int argsize,
boolean persistent)
{
Map<String, Long> addrs = null;
if (persistent)
{
// check if the library already has an address cache
addrs = addrcache.get(module);
// try to find the address in the cache, if there is a cache
if (addrs != null)
{
Long cached = addrs.get(generateAddressCacheKey(funcname, ordinal));
if (cached != null)
return cached;
}
}
long funcaddr = 0;
// at this point we know we need to try to find the function address
if (platform == PLATFORM_WINDOWS && ordinal != -1)
{
funcaddr = findByOrdinal(module, ordinal);
}
else
{
funcaddr = findByName(module, funcname);
// try the decorated version (the 4GL does this same thing) if the given name
// failed
if (funcaddr == 0 && platform == PLATFORM_WINDOWS && stdcall)
{
funcaddr = findByName(module, String.format("_%s@%d", funcname, argsize));
}
}
// cache it only if we are persistent AND the address was found successfully
// (we wouldn't be here if it was already cached)
if (persistent && funcaddr != 0)
{
if (addrs == null)
{
addrs = Collections.synchronizedMap(new HashMap<String, Long>());
addrcache.put(module, addrs);
}
addrs.put(generateAddressCacheKey(funcname, ordinal), funcaddr);
}
return funcaddr;
}
/**
* Calculate the key that will be used to access the given function in the address
* cache.
*
* @param funcname
* The function name.
* @param ordinal
* The ordinal that the API is exported as (on Windows this is used in preference
* to the name) or -1 if no ordinal was specified.
*
* @return The address cache key.
*/
private static String generateAddressCacheKey(String funcname, int ordinal)
{
String key = funcname;
if (platform == PLATFORM_WINDOWS && ordinal != -1)
{
key = String.format("%d", ordinal);
}
return key;
}
/**
* Convert the calling convention enum into an integer constant version that can be
* passed to the native layer.
*
* @param conv
* The calling convention enum.
*
* @return The equivalent constant.
*/
private static int calcCallingConv(CallingConvention conv)
{
int callconv = CONVENTION_DEFAULT;
switch (conv)
{
case STDCALL:
callconv = CONVENTION_STDCALL;
break;
case CDECL:
callconv = CONVENTION_CDECL;
break;
case PASCAL:
callconv = CONVENTION_PASCAL;
break;
}
return callconv;
}
}