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;
   }
}