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