# NativeInvoker.java

    /*
    ** Module   : NativeInvoker.java
    ** Abstract : support for invocation of shared library functions
    **
    ** Copyright (c) 2013-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
    ** 001 GES 20140116 First version.
    ** 002 GES 20140127 Fixed memptr processing and pending error detection logic in
    **                  copyDataOutWorker().
    ** 003 GES 20140212 Fixed the copy back of return data and the processing for output arrays.
    **                  Lots of tweaks to error processing (eliminating prefixes from error messages
    **                  and ensuring that unsuppressed errors get generated at the right times).
    **                  Fixed output parameter processing for integers set to unknown value. Added
    **                  processing to fixup input parameters that are not BDT, but are Java wrappers
    **                  (like Integer). Fixed integer copy in processing for scalar instances.
    ** 004 HC  20140613 Improved extent parameter support - introduced multiple wrapper classes
    **                  depending on the parameter type and the l-value type (variable vs.
    **                  field).
    ** 005 ECF 20150715 Replace StringBuffer with StringBuilder.
    ** 006 GES 20171220 Changes to match new silent mode implementation.
    ** 007 CA  20181029 Some changes to allow native API calls via the CALL handle's DLL-CALL-TYPE
    **                  mode.
    ** 008 CA  20210609 Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly
    **                  initialized at the method's execution, and not at the caller's arguments.
    **     CA  20220515 Allow library and memptr calls to be executed on server-side.
    **     CA  20220602 A memptr returned by a native library call will not have its size set.  Removed the length
    **                  validation for memptr arguments.
    ** 009 SVL 20230803 genInputUnknownErr was made public.
    ** 010 GBB 20240826 OSResourceManager moved to a new package osresource.
    */

    /*
    ** 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 java.math.*;

    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.osresource.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.util.*;

    /**
     * Server-side support for native library management and function invocation.  As much as is
     * possible, parameter validation, signature normalization and error processing will occur
     * in the class.  All library loading/unloading, function address lookup, marshalling/
     * unmarshalling of parameters and the actual invocation of the function will be done on the
     * client-side (see {@link LibraryDaemon} and {@link LibraryManager}).
     * <p>
     * All the functionality of this class (actually, of the entire package) is exposed via two
     * methods, {@link #invoke} and {@link #release}.
     */
    public class NativeInvoker
    {
       /** Stores context local instances of the remote memory manager proxy. */
       private static final ContextLocal<WorkArea> ctxt = new ContextLocal<WorkArea>()
       {
          protected WorkArea initialValue()
          {
             WorkArea wa = new WorkArea();

             wa.libmgr = OSResourceManager.getLibrary();

             return wa;
          }
       };

       /**
        * Instances of this class cannot be constructed.
        */
       private NativeInvoker()
       {
       }

       /**
        * Invoke the defined native API call in a manner that is compatible with how the 4GL behaves.
        *
        * @param    libname
        *           The library name where this native procedure should be found.
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        * @param    signInput
        *           The signature represented by the caller's passed arguments. This is NOT the same
        *           as the actual signature of native API to be called. This will be compared to the
        *           native API's required signature and errors will be raised as needed.
        * @param    modes
        *           An encoded string with the mode of each RUN parameter specified as a character
        *           in the string.  This is what was defined on the RUN statement NOT what was
        *           defined in the PROCEDURE EXTERNAL.
        * @param    argsInput
        *           Arguments that were passed (at runtime) to be the inputs to (and receive the
        *           outputs from) the API call.
        */
       public static void invoke(String     libname,
                                 String     pname,
                                 String     iename,
                                 Class<?>[] signInput,
                                 String     modes,
                                 Object...  argsInput)
       {
          InternalEntry ie = SourceNameMapper.getInternalEntry(pname, iename, false);

          if (ie == null)
          {
             // this is some kind of conversion or configuration problem, since we should never be
             // called unless there is a matching definition
             String err1 = "Missing InternalEntry for library " + libname + " and external proc " +
                           pname + " and internal entry name " + iename;
             throw new IllegalStateException(err1);
          }

          NativeAPIEntry calldef = null;

          if (ie instanceof NativeAPIEntry)
          {
             calldef = (NativeAPIEntry) ie;
          }

          if (calldef == null)
          {
             // this is some kind of conversion or configuration problem, since we should never be
             // called unless there is a matching definition
             String err2 = "Missing NativeAPIEntry for library " + libname + " and external proc " +
                           pname + " and internal entry name " + iename;
             throw new IllegalStateException(err2);
          }

          invoke(calldef, false, pname, signInput, modes, argsInput);
       }

       /**
        * Invoke the defined native API call in a manner that is compatible with how the 4GL behaves.
        *
        * @param    calldef
        *           The native API definition.
        * @param    noInvoke
        *           Flag indicating the native API will not be invoked, just resolved (and its library
        *           loaded).
        * @param    signInput
        *           The signature represented by the caller's passed arguments. This is NOT the same
        *           as the actual signature of native API to be called. This will be compared to the
        *           native API's required signature and errors will be raised as needed.
        * @param    modes
        *           An encoded string with the mode of each RUN parameter specified as a character
        *           in the string.  This is what was defined on the RUN statement NOT what was
        *           defined in the PROCEDURE EXTERNAL.
        * @param    argsInput
        *           Arguments that were passed (at runtime) to be the inputs to (and receive the
        *           outputs from) the API call.
        */
       public static void invoke(NativeAPIEntry calldef,
                                 boolean        noInvoke,
                                 String         pname,
                                 Class<?>[]     signInput,
                                 String         modes,
                                 Object...      argsInput)
       {
          String libname = calldef.getLibraryName();
          String iename = calldef.getLegacyName();

          // at this point, calldef should be an existing native API definition
          boolean           persistent = calldef.isPersistent();
          CallingConvention callconv   = calldef.getCallingConvention();
          int               ordinal    = calldef.getOrdinal();
          List<Parameter>   parms      = calldef.getParameterList();
          String            legacy     = calldef.getLegacyName();
          Signature         briefcase  = new Signature();
          Class<?>[]        signature  = new Class<?>[signInput.length];
          Object[]          args       = new Object[argsInput.length];

          // before we can properly process the arguments, we must eliminate any use of the
          // OutputExtentParameter container class
          fixupArrays(signInput, signature, argsInput, args);

          // input parameters can be passed as literals in the 4GL, some of which will emit
          // in Java as literals instead of BDT wrappers; with autoboxing the resulting instances
          // may include String, Byte, Short, Integer, Long, Float and Double; these will be
          // translated to the proper BDT wrapper so that downstream processing is simpler
          fixupNonWrappers(signature, args);

          boolean fail = true;
          briefcase.setNoInvoke(noInvoke);

          // do inbound error checking, prepare the return value/parameter descriptors and copy
          // the input data into the descriptors
          if (inboundProcessing(pname, iename, parms, signature, modes, briefcase, args))
          {
             fail = false;
             return;
          }

          NativeAPICaller libmgr = ctxt.get().libmgr;

          // make the call to the client-side (we use the "legacy" name from the original def
          // rather than the RUN statement iename, since the iename can have non-matching case
          // but the legacy name must have matched the library's exported name exactly)
          briefcase = libmgr.invoke(libname, legacy, ordinal, persistent, callconv, briefcase);

          int rc = briefcase.getErrorCode();

          // error processing
          if (rc != LibraryManager.ERROR_NONE)
          {
             switch (rc)
             {
                case LibraryManager.ERROR_LIBRARY_LOAD:
                   genLibraryLoadErr(libname, briefcase.getPlatform());
                   break;
                case LibraryManager.ERROR_MISSING_ENTRYPOINT:
                   genEntrypointErr(iename, ordinal);
                   break;
                // none of these cases should ever occur, so we throw a "real" java exception
                case LibraryManager.ERROR_INVALID_CALL_CONV:
                case LibraryManager.ERROR_INVALID_TYPE:
                case LibraryManager.ERROR_UNKNOWN_FAILURE:
                case LibraryManager.ERROR_OUT_OF_MEMORY:
                   String msg = String.format("Unexpected error in JNI dispatch(), rc = %d", rc);
                   throw new IllegalArgumentException(msg);
             }

             fail = false;
             return;
          }

          // outbound checks and copy data back
          outboundProcessing(parms, signature, briefcase, args);
          fail = false;
       }

       /**
        * Attempts to unload the specified library from memory (this is the implementation of the
        * RELEASE EXTERNAL statement).  This code delegates processing the to client-side.
        *
        * @param    libname
        *           The name of the library to attempt to unload.
        */
       public static void release(String libname)
       {
          ctxt.get().libmgr.release(libname);
       }

       /**
        * Generate an input value cannot be unknown error.
        *
        * @param    force
        *           <code>true</code> to throw the error even in silent mode.
        */
       public static void genInputUnknownErr(boolean force)
       {
          String spec = "A variable or array element passed as an INPUT or INPUT-OUTPUT parameter " +
                        "to a DLL cannot contain the Unknown value";

          genForcableError(12272, spec, force);
       }

       /**
        * Eliminate any use of the OutputExtentParameter container class and replace it with the
        * actual array references.  The passed references will be edited if necessary.
        *
        * @param    signIn
        *           The signature represented by the caller's passed arguments.
        * @param    signOut
        *           The fixed signature.
        * @param    argsIn
        *           Arguments that were passed (at runtime) to be the inputs to (and receive the
        *           outputs from) the API call.
        * @param    argsOut
        *           The fixed arguments.
        */
       private static void fixupArrays(Class<?>[] signIn,
                                       Class<?>[] signOut,
                                       Object[]   argsIn,
                                       Object[]   argsOut)
       {
          for (int i = 0; i < argsIn.length; i++)
          {
             if (argsIn[i] instanceof AbstractExtentParameter)
             {
                AbstractExtentParameter oep = (AbstractExtentParameter) argsIn[i];

                argsOut[i] = oep.getVariableSafe();
                signOut[i] = argsOut[i].getClass();

                // disable the logic that tries to change the reference since we will never
                // have an indeterminate sized extent
                oep.setParameter(oep.getVariableSafe());
             }
             else
             {
                argsOut[i] = argsIn[i];
                signOut[i] = signIn[i];
             }
          }
       }

       /**
        * Fixup any input parameters which were passed as literals in the 4GL, which emitted
        * as Java literals instead of BDT wrappers.  With autoboxing the resulting instances
        * may include String, Byte, Short, Integer, Long, Float and Double.  These will be
        * translated to the proper BDT wrapper so that downstream processing is simpler.
        * <p>
        * At this time there is no actual checking of whether this is really an input mode parm
        * or what the exact type should be. If there is a mismatch on the mode, then the caller
        * has coded something that can never receive an update and they will have to figure that
        * out themselves (but it should never happen in converted code since the 4GL disallows
        * literals except for input parms). If there is a type mismatch, then it will trigger a
        * failure downstream in a manner that is 4GL compatible.
        *
        * @param    signature
        *           The signature represented by the caller's passed arguments.  This will be edited
        *           directly to fixup any issues.
        * @param    args
        *           Arguments that were passed (at runtime) to be the inputs to (and receive the
        *           outputs from) the API call.   This will be edited directly to fixup any issues.
        */
       private static void fixupNonWrappers(Class<?>[] signature, Object[] args)
       {
          for (int i = 0; i < signature.length; i++)
          {
             if (signature[i].equals(Byte.class)    ||
                 signature[i].equals(Short.class)   ||
                 signature[i].equals(Integer.class) ||
                 signature[i].equals(Long.class))
             {
                signature[i] = int64.class;
                args[i]      = new int64((Number) args[i]);
             }
             else if (signature[i].equals(Float.class) || signature[i].equals(Double.class))
             {
                signature[i] = decimal.class;
                args[i]      = new decimal((Number) args[i]);
             }
             else if (signature[i].equals(String.class))
             {
                signature[i] = character.class;
                args[i]      = new character((String) args[i]);
             }
          }
       }

       /**
        * Checks the passed parameters (and their values) against the definition and raises errors
        * to match the behavior of the 4GL, if OK the data will be copied into the descriptors for
        * transport.
        * <p>
        * The following checks are done (in order):
        * <ol>
        *    <li> unknown value, uninitialized pointer and empty string tests
        *    <li> type and mode of the parameters
        *    <li> value of the parameters
        *    <li> number of parameters
        * </ol>
        * <p>
        * This code (and its downstream workers) also handles the proper setup of the descriptors
        * for the return value and the parameters.  Those descriptors define whether the particular
        * parameter needs to be passed as a pointer, whether it copies data back and other metadata
        * as well as containing the actual value(s) to be transported to the client.
        * <p>
        * The complexity and quirkiness of the error checking naturally makes this the proper place
        * to setup the descriptors (including copying in the input data).
        *
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        * @param    defs
        *           Parameter definitions. This defines the required signature of the actual native
        *           API as defined by a converted 4GL <code>PROCEDURE EXTERNAL</code> statement. As
        *           such it may or may not match reality, but it is what the 4GL uses for error
        *           checking (and there isn't anything better that we can do).
        * @param    signature
        *           The class list represented by the caller's passed arguments (determined at
        *           runtime). This is NOT the same as the actual signature of native API to be
        *           called. This will be compared to the native API's required signature and errors
        *           will be raised as needed.
        * @param    modetxt
        *           An encoded string with the mode of each RUN parameter specified as a character
        *           in the string.  This is what was defined on the RUN statement NOT what was
        *           defined in the PROCEDURE EXTERNAL.
        * @param    briefcase
        *           The set of return value and parameter descriptors which should be setup for
        *           transport to the client.  This is initially passed as an empty instance and
        *           if no errors are encountered, it will be fully configured for transport to the
        *           client on return.
        * @param    args
        *           Arguments that were passed (at runtime) to be the inputs to (and receive the
        *           outputs from) the API call.
        *
        * @return   <code>true</code> to cause a silent return from the calling method. Of course, if
        *           and error has been raised, then the exception will be unwinding the stack and the
        *           return won't matter. But in the case where silent error mode is enabled (the 4GL
        *           <code>NO-ERROR</code> case), we must return back silently.
        */
       private static boolean inboundProcessing(String          pname,
                                                String          iename,
                                                List<Parameter> defs,
                                                Class<?>[]      signature,
                                                String          modetxt,
                                                Signature       briefcase,
                                                Object...       args)
       {
          int numDefs = defs.size();

          // initialize the argument list
          initializeBriefcase(defs, signature, briefcase, args);

          // we only error check the smaller of the number of defined parms or passed parms
          int numCheck = Math.min(numDefs, signature.length);

          int offset = 0;

          // check 1: unknown value and empty string tests
          for (int i = 0; i < numCheck; i++)
          {
             Parameter      parm = defs.get(i);
             String         mode = parm.getMode();
             BaseNativeType bnt  = null;

             if ("RETURN".equalsIgnoreCase(mode))
             {
                // compensate for the missing return parameter when looking up the BNT
                offset++;
                bnt = briefcase.getReturnValue();
             }
             else
             {
                bnt = briefcase.getArgument(i - offset);
             }

             // avoid NPE in checkUnknown, we will fail later in inbound processing during the
             // type checking
             if (bnt == null)
                continue;

             // this also will edit the descriptor(s) if needed to disable copying back as matches
             // the 4GL behavior (it is much better to do it here than to duplicate this logic
             // elsewhere)
             if (checkUnknown(pname, iename, parm.getMode(), signature[i], args[i], bnt))
                return true;
          }

          // make sure our encoded mode text is regular
          if (modetxt == null)
          {
             modetxt = StringHelper.repeatChar(InternalEntry.INPUT_MODE, numDefs);
          }
          else
          {
             int gap = numDefs - modetxt.length();

             if (gap > 0)
             {
                StringBuilder sb = new StringBuilder(modetxt);
                StringHelper.repeatChar(InternalEntry.INPUT_MODE, gap, sb);
                modetxt = sb.toString();
             }
          }

          // check 2: type and mode of the parameters
          for (int j = 0; j < numCheck; j++)
          {
             Parameter parm = defs.get(j);
             Class<?> real = signature[j].isArray() ? signature[j].getComponentType() : signature[j];

             if (checkModeAndType(pname, iename, parm, modetxt.charAt(j), real))
                return true;
          }

          // reset our offset processing
          offset = 0;

          // check 3: value of the parameters (only input and input-output integer data is checked,
          // as well as uninitialized memptr and some array errors)
          for (int k = 0; k < numCheck; k++)
          {
             Parameter parm = defs.get(k);
             String    type = parm.getType();
             String    mode = parm.getMode();

             // detect use of an indeterminate array
             if (signature[k].isArray() && ArrayAssigner.isDynamicArray((BaseDataType[]) args[k]))
             {
                genIndeterminateArrayErr();
                return true;
             }

             if ("RETURN".equalsIgnoreCase(mode))
             {
                if (signature[k].isArray())
                {
                   genNoArrayForReturnErr();
                   return true;
                }

                // compensate for the missing return parameter when looking up the BNT
                offset++;
                continue;
             }

             BaseNativeType bnt = briefcase.getArgument(k - offset);

             if ("INPUT".equals(mode) || "INPUT-OUTPUT".equals(mode))
             {
                Class<?> real = signature[k].isArray() ? signature[k].getComponentType()
                                                       : signature[k];

                if (real.equals(integer.class) || real.equals(int64.class))
                {
                   if (checkIntegerRange(type, signature[k], args[k]))
                      return true;
                }
                else if (signature[k].equals(memptr.class))
                {
                   // the above test is deliberately done against the original class instead of
                   // the "real" class because this error is only raised when we are dealing with
                   // the scalar (non-array) case; that is why it is safe to cast this directly
                   // to a memptr when normally we could have either a memptr or memptr[]
                   memptr ptr = (memptr) args[k];

                   if (ptr.isUninitialized()) //  || ptr.lengthOf() == 0)
                   {
                      genUninitMemptrErr(pname, iename);
                      return true;
                   }
                }

                // now we have passed our input checks, let's copy our data in for this parm
                copyDataIn(type, mode, signature[k], args[k], bnt);
             }

             // pass-by-pointer cases are special because they actually copy in as well; if
             // we don't copy our data in then the native call will trap badly because there
             // won't be any buffer to access
             if ("OUTPUT".equals(mode) && bnt.isPassByPointer())
             {
                copyDataIn(type, mode, signature[k], args[k], bnt);
             }
          }

          // check 4: mismatching number of parameters (this could have been done first, but in the
          // 4gl it is last), this includes the return parm if it exists
          if (numDefs != signature.length)
          {
             String spec = "Mismatched number of parameters passed to routine %s %s";
             ErrorManager.recordOrThrowError(3234, String.format(spec, iename, pname), false);
             return true;
          }

          return false;
       }

       /**
        * Analyze the required parameter signature for the function and the passed arguments and
        * use that information to setup the initial configuration of the briefcase.
        *
        * @param    defs
        *           Parameter definitions. This defines the required signature of the actual native
        *           API as defined by a converted 4GL <code>PROCEDURE EXTERNAL</code> statement.
        * @param    signature
        *           The class list represented by the caller's passed arguments (determined at
        *           runtime). This is NOT the same as the actual signature of native API to be
        *           called. This will be compared to the native API's required signature and errors
        *           will be raised as needed.
        * @param    briefcase
        *           The set of return value and parameter descriptors which should be setup for
        *           transport to the client.  This is initially passed as an empty instance.
        * @param    args
        *           Arguments that were passed (at runtime) to be the inputs to (and receive the
        *           outputs from) the API call.
        */
       private static void initializeBriefcase(List<Parameter> defs,
                                               Class<?>[]      signature,
                                               Signature       briefcase,
                                               Object...       args)
       {
          int numDefs = defs.size();
          int offset  = 0;

          briefcase.setArgumentSize(calcNumArguments(defs));

          // we can only process the smaller of these values, the code will fail downstream
          // with a mismatching number of parms
          int num = Math.min(numDefs, signature.length);

          for (int j = 0; j < num; j++)
          {
             Parameter parm = defs.get(j);

             String  type     = parm.getType();
             String  mode     = parm.getMode();
             String  attr     = parm.getAttribute("handle_to");
             boolean handleTo = "true".equalsIgnoreCase(attr);

             BaseNativeType bnt = null;

             if (signature[j].isArray())
             {
                BaseDataType[]   bdtArray = (BaseDataType[]) args[j];
                BaseNativeType[] bntArray = new BaseNativeType[bdtArray.length];

                NativeTypeArray nta = new NativeTypeArray();

                boolean copyIn  = false;
                boolean copyOut = false;

                if ("INPUT".equalsIgnoreCase(mode) || "INPUT-OUTPUT".equalsIgnoreCase(mode))
                {
                   copyIn = true;
                }

                if ("OUTPUT".equalsIgnoreCase(mode) || "INPUT-OUTPUT".equalsIgnoreCase(mode))
                {
                   copyOut = true;
                }

                // no RETURN processing is needed, return mode can't be EXTENT

                nta.setCopyIn(copyIn);
                nta.setCopyOut(copyOut);
                nta.setPassByPointer(true);

                // initialize the array of elements
                for (int k = 0; k < bntArray.length; k++)
                {
                   bntArray[k] = createScalarInstance(type, mode, handleTo);

                   if (bntArray[k] instanceof NativeInteger)
                   {
                      NativeInteger ni = (NativeInteger) bntArray[k];
                      ni.setInArray(true);
                   }
                }

                // save the elements off
                nta.setElements(bntArray);
                bnt = nta;
             }
             else
             {
                bnt = createScalarInstance(type, mode, handleTo);
             }

             if ("RETURN".equalsIgnoreCase(mode))
             {
                briefcase.setReturnValue(bnt);
                offset++;
             }
             else
             {
                // we use an index offset to compensate for a return value that is arbitrary
                // inserted into the parameter list (instead of keeping it separate, the 4GL
                // allows the programmer to define it in an arbitrary position)
                briefcase.setArgument(bnt, (j - offset));
             }
          }
       }

       /**
        * Return back the non-negative number of arguments for this native API call by processing
        * the parameter definitions and excluding any RETURN parameter that is found.
        *
        * @param    defs
        *           Parameter definitions. This defines the required signature of the actual native
        *           API as defined by a converted 4GL <code>PROCEDURE EXTERNAL</code> statement.
        *
        * @return   The non-negative number of non-RETURN arguments.
        */
       private static int calcNumArguments(List<Parameter> defs)
       {
          if (defs == null)
          {
             return 0;
          }

          int max = defs.size();

          for (int i = 0; i < max; i++)
          {
             Parameter parm = defs.get(i);

             if ("RETURN".equalsIgnoreCase(parm.getMode()))
             {
                max--;
                break;
             }
          }

          return max;
       }

       /**
        * Instantiate a placeholder for a single scalar parameter and configure it based on the
        * type and mode.
        *
        * @param    type
        *           The 4GL "DLL" data type.
        * @param    mode
        *           The 4GL parameter mode (e.g. INPUT).
        * @param    handleTo
        *           <code>true</code> if HANDLE TO was specified.
        *
        * @return   The new placeholder instance.
        */
       private static BaseNativeType createScalarInstance(String type, String mode, boolean handleTo)
       {
          BaseNativeType bnt = null;

          boolean copyIn  = false;
          boolean copyOut = false;
          boolean asPtr   = false;

          if ("INPUT".equalsIgnoreCase(mode))
          {
             copyIn = true;
             asPtr  = handleTo;
          }

          if ("INPUT-OUTPUT".equalsIgnoreCase(mode))
          {
             copyIn = true;
          }

          if ("OUTPUT".equalsIgnoreCase(mode) || "INPUT-OUTPUT".equalsIgnoreCase(mode) ||
              "RETURN".equalsIgnoreCase(mode))
          {
             copyOut = true;
          }

          if ("CHARACTER".equalsIgnoreCase(type) || "LONGCHAR".equalsIgnoreCase(type))
          {
             bnt = new NativeString();
             asPtr = true;

             if ("RETURN".equalsIgnoreCase(mode))
             {
                copyOut = false;
             }
          }
          else if ("LONG".equalsIgnoreCase(type))
          {
             bnt = new NativeSignedInt32();
          }
          else if ("UNSIGNED-LONG".equalsIgnoreCase(type))
          {
             bnt = new NativeUnsignedInt32();
          }
          else if ("SHORT".equalsIgnoreCase(type))
          {
             bnt = new NativeSignedInt16();
          }
          else if ("UNSIGNED-SHORT".equalsIgnoreCase(type))
          {
             bnt = new NativeUnsignedInt16();
          }
          else if ("INT64".equalsIgnoreCase(type))
          {
             bnt = new NativeSignedInt64();
          }
          else if ("BYTE".equalsIgnoreCase(type))
          {
             bnt = new NativeInt8();
          }
          else if ("DOUBLE".equalsIgnoreCase(type))
          {
             bnt = new NativeDouble();
          }
          else if ("FLOAT".equalsIgnoreCase(type))
          {
             bnt = new NativeFloat();
          }
          else if ("MEMPTR".equalsIgnoreCase(type))
          {
             bnt = new NativeBuffer();
             asPtr = true;
          }

          // avoid NPE, we will fail later in inbound processing at the type checking logic
          if (bnt != null)
          {
             // configure the type
             bnt.setCopyIn(copyIn);
             bnt.setCopyOut(copyOut);
             bnt.setPassByPointer(asPtr);
          }

          return bnt;
       }

       /**
        * This drives the unknown, uninitialized and empty string checking for a given argument
        * and generates errors in a 4GL compatible manner.  Calls {@link #checkUnknownWorker}
        * for the core processing.
        *
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        * @param    mode
        *           "INPUT", "INPUT-OUTPUT", "OUTPUT" or "RETURN".
        * @param    cls
        *           The data type of the argument that was passed, which may be an array.
        * @param    arg
        *           The actual argument instance that was passed, which may be an array.
        * @param    bnt
        *           The descriptor for this scalar parameter or for an array. Some cases of
        *           unknown value, uninitialized variable or empty string will cause the normal
        *           copy back behavior to be silently ignored instead of raising an error. This
        *           method will edit the descriptor(s) as needed based on the actual value passed.
        *
        * @return   <code>true</code> to cause a silent return from the calling method.
        */
       private static boolean checkUnknown(String         pname,
                                           String         iename,
                                           String         mode,
                                           Class<?>       cls,
                                           Object         arg,
                                           BaseNativeType bnt)
       {
          boolean array = cls.isArray();

          if (array)
          {
             Class<?> real  = cls.getComponentType();
             Object[] args  = (Object[]) arg;

             // if the bnt is not an array we have a serious issue; we also assume it will have
             // the same number of elements as our given variable
             NativeTypeArray  nta   = (NativeTypeArray) bnt;
             BaseNativeType[] descr = nta.getElements();

             for (int i = 0; i < args.length; i++)
             {
                if (checkUnknownWorker(pname, iename, mode, real, args[i], true, descr[i]))
                {
                   return true;
                }
             }

             return false;
          }
          else
          {
             return checkUnknownWorker(pname, iename, mode, cls, arg, false, bnt);
          }
       }

       /**
        * This checks a single argument or a single argument array element to detect if it is
        * unknown or empty string and generate errors in a 4GL compatible manner.  Called by
        * {@link #checkUnknown}.
        *
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        * @param    mode
        *           "INPUT", "INPUT-OUTPUT", "OUTPUT" or "RETURN".
        * @param    cls
        *           The data type of the argument that was passed, which may be an element of an
        *           array.
        * @param    arg
        *           The actual argument instance that was passed, which may be an element of an
        *           array.
        * @param    array
        *           <code>true</code> if this instance was an element of an array.
        * @param    bnt
        *           The descriptor for this scalar parameter (or array element). Some cases of
        *           unknown value, uninitialized variable or empty string will cause the normal
        *           copy back behavior to be silently ignored instead of raising an error. This
        *           method will edit the descriptor as needed based on the actual value passed.
        *
        * @return   <code>true</code> to cause a silent return from the calling method.
        */
       private static boolean checkUnknownWorker(String         pname,
                                                 String         iename,
                                                 String         mode,
                                                 Class<?>       cls,
                                                 Object         arg,
                                                 boolean        array,
                                                 BaseNativeType bnt)
       {
          boolean in    = mode.equals("INPUT");
          boolean inOut = mode.equals("INPUT-OUTPUT");
          boolean out   = mode.equals("OUTPUT");
          boolean ret   = mode.equals("RETURN");

          BaseDataType bdt = null;

          if (arg instanceof BaseDataType)
          {
             bdt = (BaseDataType) arg;
          }
          else
          {
             // anything else is going to fail downstream (as is done in the 4gl)
             return false;
          }

          if (cls.equals(integer.class) || cls.equals(int64.class))
          {
             if (bdt.isUnknown())
             {
                if ((in || inOut))
                {
                   genInputUnknownErr(true);
                   return true;
                }
                else if (out)
                {
                   // int/int64 output mode with unknown calls API but doesn't copy back
                   bnt.setCopyOut(false);

                   // must force pointer mode becuase otherwise the parm won't render properly
                   // on the client side (it won't render as a pointer)
                   bnt.setPassByPointer(true);
                }

                // int/int64 return mode with unknown calls API and DOES copy back
             }
          }
          else if (cls.equals(decimal.class))
          {
             if (bdt.isUnknown())
             {
                if ((in || inOut))
                {
                   genInputUnknownErr(true);
                   return true;
                }

                // dec output mode and return mode with unknown calls API and DOES copy back
             }
          }
          else if (cls.equals(character.class))
          {
             if (bdt.isUnknown())
             {
                if (in || inOut)
                {
                   genInputUnknownErr(true);
                   return true;
                }
                else if (out || ret)
                {
                   genCharUnknownErr(true);
                   return true;
                }
             }
             else
             {
                // empty string case calls API but doesn't copy back
                if (TextOps.lengthOf((character) bdt) == 0)
                {
                   bnt.setCopyOut(false);
                }
             }
          }
          else if (cls.equals(longchar.class))
          {
             if (bdt.isUnknown())
             {
                if (in || inOut)
                {
                   if (array)
                   {
                      genInputUnknownErr(true);
                      return true;
                   }
                   else
                   {
                      // non-array elements have suppressable errors in this case
                      genCharUnknownErr(false);
                      return true;
                   }
                }
                else if (out)
                {
                   // output elements have suppressable errors for the non-array case and
                   // unsuppressable errors in an array
                   genCharUnknownErr(array);
                   return true;
                }
                else if (ret)
                {
                   // return mode case calls API but doesn't copy back
                   bnt.setCopyOut(false);
                }
             }
             else
             {
                if (TextOps.lengthOf((longchar) bdt) == 0)
                {
                   if (in || inOut || out)
                   {
                      genCharUnknownErr(false);
                      return true;
                   }
                   else if (ret)
                   {
                      // empty string return case calls API but doesn't copy back
                      bnt.setCopyOut(false);
                   }
                }
             }
          }
          else if (cls.equals(memptr.class))
          {
             if (bdt.isUnknown())
             {
                if (in || inOut)
                {
                   genInputUnknownErr(true);
                   return true;
                }

                // unknown value as an array element is passed as a null pointer but for
                // a scalar instance it raises an error
                if (out && !array)
                {
                   genUninitMemptrErr(pname, iename);
                   return true;
                }
             }
             else
             {
                // INPUT and INPUT-OUTPUT cases will be processed later (during value checking)

                // uninitialized/0-size as an array element is passed as a null pointer but for
                // a scalar instance it raises an error
                if (out && !array)
                {
                   memptr ptr = (memptr) bdt;

                   if (ptr.isUninitialized() || ptr.lengthOf() == 0)
                   {
                      genUninitMemptrErr(pname, iename);
                      return true;
                   }
                }
             }

             // return case that is uninitialized works fine (address is copied back)
             // return case that is unknown or 0 size actually abends 4gl (we will implement these
             // like the uninitialized case since the 4gl can't work)
          }

          // anything else is going to fail downstream (as is done in the 4gl)
          return false;
       }

       /**
        * This drives the data type and mode checking for a given argument and will generate errors
        * in a 4GL compatible manner.  First the RUN statement mode is compared against the defined
        * mode for the corresponding DEFINE PARAMETER in the PROCEDURE EXTERNAL statement.  Second,
        * the data type of the actual runtime (RUN statement) argument is compared with the defined
        * data type in the corresponding DEFINE PARAMETER in the PROCEDURE EXTERNAL statement. The
        * first failure found is raised as an error.
        *
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        * @param    parm
        *           The parameter definition.
        * @param    runmode
        *           'I' for "INPUT", 'U' for "INPUT-OUTPUT" or 'O' for "OUTPUT". "RETURN" parameters
        *           are passed at runtime as "OUTPUT").
        * @param    cls
        *           The data type of the argument that was passed (or the type of the element of the
        *           array that was passed, which is the type to be considered.
        *
        * @return   <code>true</code> to cause a silent return from the calling method.
        */
       private static boolean checkModeAndType(String    pname,
                                               String    iename,
                                               Parameter parm,
                                               char      runmode,
                                               Class<?>  cls)
       {
          // RUN statement modes don't use RETURN, they are always specified as OUTPUT; for this
          // reason, we use an alternate form of the getter for the mode, which will return OUTPUT
          // for both OUTPUT and RETURN parms; this simplifies the logic below
          String mode = parm.getSignatureMode();

          // check our mode before checking the type
          if ((runmode == InternalEntry.INPUT_MODE        && !"INPUT".equals(mode))        ||
              (runmode == InternalEntry.INPUT_OUTPUT_MODE && !"INPUT-OUTPUT".equals(mode)) ||
              (runmode == InternalEntry.OUTPUT_MODE       && !"OUTPUT".equals(mode)))
          {
             genMismatchingModeErr(pname, iename);
             return true;
          }

          String  type    = parm.getType().toUpperCase();
          boolean typeErr = true;

          // now check the type
          if (cls.equals(integer.class) || cls.equals(int64.class))
          {
             if ("BYTE".equals(type)           ||
                 "SHORT".equals(type)          ||
                 "UNSIGNED-SHORT".equals(type) ||
                 "LONG".equals(type)           ||
                 "UNSIGNED-LONG".equals(type)  ||
                 "INT64".equals(type))
             {
                typeErr = false;
             }
          }
          else if (cls.equals(decimal.class))
          {
             if ("FLOAT".equals(type) || "DOUBLE".equals(type))
             {
                typeErr = false;
             }
          }
          else if (cls.equals(character.class) || cls.equals(longchar.class))
          {
             if ("CHARACTER".equals(type))
             {
                typeErr = false;
             }
          }
          else if (cls.equals(memptr.class))
          {
             if ("MEMPTR".equals(type))
             {
                typeErr = false;
             }
          }

          if (typeErr)
          {
             genMismatchingTypeErr(pname, iename);
             return true;
          }

          return false;
       }

       /**
        * Check if the given argument (or if it is an array, if the argument's elements) is within a
        * valid range for the type specified. Any out of range value will raise an error (except in
        * silent error mode).
        * <p>
        * <pre>
        * BYTE: -128 through 255
        * SHORT: -32768 through 32767
        * UNSIGNED-SHORT: 0 through 65535
        * LONG: -2147483648 through 2147483647
        * UNSIGNED-LONG: 0 through 4294967295
        * </pre>
        *
        * @param    type
        *           The library function's parameter type as defined in the DEFINE PARAMETER
        *           statement.
        * @param    cls
        *           The class of the argument, which may be an array type.
        * @param    arg
        *           The argment to be checked.
        *
        * @return   <code>true</code> if the value is out of bounds.
        */
       private static boolean checkIntegerRange(String type, Class<?> cls, Object arg)
       {
          // quick out if the library type is int64 since that cannot overflow or underflow
          if ("INT64".equals(type))
             return false;

          boolean array = cls.isArray();

          if (array)
          {
             Object[] args = (Object[]) arg;

             for (int i = 0; i < args.length; i++)
             {
                int64 value = (int64) args[i];

                if (checkIntegerRangeWorker(type, value.longValue()))
                {
                   return true;
                }
             }

             return false;
          }
          else
          {
             int64 value = (int64) arg;
             return checkIntegerRangeWorker(type, value.longValue());
          }
       }

       /**
        * Check if the given value is within a valid range for the type specified. Any out of range
        * value will raise an error (except in silent error mode).
        * <p>
        * <pre>
        * BYTE: -128 through 255
        * SHORT: -32768 through 32767
        * UNSIGNED-SHORT: 0 through 65535
        * LONG: -2147483648 through 2147483647
        * UNSIGNED-LONG: 0 through 4294967295
        * </pre>
        *
        * @param    type
        *           The library function's parameter type as defined in the DEFINE PARAMETER
        *           statement.
        * @param    value
        *           The value to be checked.
        *
        * @return   <code>true</code> if the value is out of bounds.
        */
       private static boolean checkIntegerRangeWorker(String type, long value)
       {
          if (("BYTE".equals(type)           && (value < -128 || value > 255))               ||
              ("SHORT".equals(type)          && (value < -32768 || value > 32767))           ||
              ("UNSIGNED-SHORT".equals(type) && (value < 0 || value > 65535))                ||
              ("LONG".equals(type)           && (value < -2147483648 || value > 2147483647)) ||
              ("UNSIGNED-LONG".equals(type)  && (value < 0 || value > 4294967295L)))
          {
             // TODO: does this error have the same flaw as 15747 where Long.MIN_VALUE will display
             // as "-(" instead of "-9223372036854775808"?
             String spec = "Value %d does not fit in %s DLL datatype";
             ErrorManager.recordOrThrowError(13712,
                                             String.format(spec, value, type.toLowerCase()),
                                             false);
             return true;
          }

          return false;
       }

       /**
        * Copy the data for the given INPUT or INPUT-OUTPUT argument (or if it is an array, the
        * argument's elements) into the container for transport to the client.
        * <p>
        * OUTPUT parameters that are passed by pointer must also have their data copied in.
        *
        * @param    type
        *           The library function's parameter type as defined in the DEFINE PARAMETER
        *           statement.
        * @param    mode
        *           The parameter's mode (e.g. INPUT, INPUT-OUTPUT, OUTPUT, RETURN).
        * @param    cls
        *           The class of the argument, which may be an array type.
        * @param    arg
        *           The argment to be copied from.
        * @param    bnt
        *           The target to which the data will be copied.
        */
       private static void copyDataIn(String         type,
                                      String         mode,
                                      Class<?>       cls,
                                      Object         arg,
                                      BaseNativeType bnt)
       {
          boolean array  = cls.isArray();

          if (array)
          {
             Class<?> real = cls.getComponentType();
             Object[] args = (Object[]) arg;

             NativeTypeArray  nta = (NativeTypeArray) bnt;
             BaseNativeType[] elements = nta.getElements();

             for (int i = 0; i < args.length; i++)
             {
                copyDataInWorker(type, mode, real, args[i], elements[i], true);
             }
          }
          else
          {
             copyDataInWorker(type, mode, cls, arg, bnt, false);
          }
       }

       /**
        * Copy the data for the given argument into the container for transport to the client.
        *
        * @param    type
        *           The library function's parameter type as defined in the DEFINE PARAMETER
        *           statement.
        * @param    mode
        *           The parameter's mode (e.g. INPUT, INPUT-OUTPUT, OUTPUT, RETURN).
        * @param    cls
        *           The class of the argument.
        * @param    arg
        *           The argment to be copied from.
        * @param    bnt
        *           The target to which the data will be copied.
        * @param    array
        *           <code>true</code> if this is an array element.
        */
       private static void copyDataInWorker(String         type,
                                            String         mode,
                                            Class<?>       cls,
                                            Object         arg,
                                            BaseNativeType bnt,
                                            boolean        array)
       {
          if (cls.equals(integer.class) || cls.equals(int64.class))
          {
             int64         ival = (int64) arg;
             NativeInteger nint = (NativeInteger) bnt;

             // if this is for an output parm and it is unknown, we are marked as pass by
             // pointer and we must bypass the actual copy which would normally happen
             if (!ival.isUnknown())
             {
                long num = ival.longValue();

                // there is a strange 4GL quirk where SCALAR INPUT BYTE data is always passed
                // as a signed byte instead of optionally being unsigned (if the original value
                // was positive); this means that data that was passed in as a positive value
                // between 128 and 255 (inclusive) will be passed as a negative value
                if ("BYTE".equals(type) && "INPUT".equals(mode) && !array)
                {
                   // the java byte is always a signed value; by casting to a byte and then
                   // back to the long, we are naturally forcing a sign extension which exactly
                   // matches the 4GL result (it doesn't change the binary representation of
                   // the number it just sign extends what is there)
                   num = (byte) num;
                }

                nint.setValue(num);
             }
          }
          else if (cls.equals(decimal.class))
          {
             if ("FLOAT".equals(type))
             {
                decimal     dval = (decimal) arg;
                NativeFloat nflt = (NativeFloat) bnt;
                BigDecimal  flt  = dval.toJavaType();

                nflt.setValue(flt.floatValue());
             }
             else if ("DOUBLE".equals(type))
             {
                decimal      dval = (decimal) arg;
                NativeDouble ndbl = (NativeDouble) bnt;

                ndbl.setValue(dval.doubleValue());
             }
          }
          else if (cls.equals(character.class))
          {
             character    ctxt = (character) arg;
             NativeString ntxt = (NativeString) bnt;

             ntxt.setValue(ctxt.toStringMessage());
          }
          else if (cls.equals(longchar.class))
          {
             longchar     ltxt = (longchar) arg;
             NativeString ntxt = (NativeString) bnt;

             ntxt.setValue(ltxt.toStringMessage());
          }
          else if (cls.equals(memptr.class))
          {
             memptr       ptr  = (memptr) arg;
             NativeBuffer nbuf = (NativeBuffer) bnt;

             nbuf.setValue(ptr);
          }
       }

       /**
        * Attempts to copy back all data (from the descriptors used for transport back to the
        * actual wrapper parameters passed in) that should be copied back, raising errors
        * to match the behavior of the 4GL when necessary.
        *
        * @param    defs
        *           Parameter definitions. This defines the required signature of the actual native
        *           API as defined by a converted 4GL <code>PROCEDURE EXTERNAL</code> statement. As
        *           such it may or may not match reality, but it is what the 4GL uses for error
        *           checking (and there isn't anything better that we can do).
        * @param    signature
        *           The class list represented by the caller's passed arguments (determined at
        *           runtime). This is NOT the same as the actual signature of native API to be
        *           called. This will be compared to the native API's required signature and errors
        *           will be raised as needed.
        * @param    briefcase
        *           The set of return value and parameter descriptors which should be setup for
        *           transport to the client.  This is initially passed as an empty instance and
        *           if no errors are encountered, it will be fully configured for transport to the
        *           client on return.
        * @param    args
        *           Arguments that were passed (at runtime) to be the inputs to (and receive the
        *           outputs from) the API call.
        */
       private static void outboundProcessing(List<Parameter> defs,
                                              Class<?>[]      signature,
                                              Signature       briefcase,
                                              Object...       args)
       {
          int numDefs = defs.size();
          int offset  = 0;

          for (int i = 0; i < numDefs; i++)
          {
             Parameter      parm = defs.get(i);
             String         type = parm.getType();
             String         mode = parm.getMode();
             BaseNativeType bnt  = null;
             boolean        ret  = false;

             if ("RETURN".equalsIgnoreCase(mode))
             {
                bnt = briefcase.getReturnValue();
                ret = true;

                // compensate for the missing return parameter when looking up the BNT
                offset++;
             }
             else if ("INPUT-OUTPUT".equals(mode) || "OUTPUT".equals(mode))
             {
                bnt = briefcase.getArgument(i - offset);
             }
             else
             {
                // INPUT case bypasses the copying below
                continue;
             }

             if (!copyDataOut(type, signature[i], ret, bnt, args[i]))
             {
                // any failure (which didn't raise an error) causes us to return silently
                // without any further processing
                return;
             }
          }
       }

       /**
        * Copy the data for the given INPUT-OUTPUT, OUTPUT or RETURN descriptor (or if it is an
        * array, the array elements) into the argument.
        *
        * @param    type
        *           The library function's parameter type as defined in the DEFINE PARAMETER
        *           statement.
        * @param    cls
        *           The class of the argument, which may be an array type.
        * @param    ret
        *           <code>true</code> if this is a RETURN parameter.
        * @param    bnt
        *           The descriptor from which to copy.
        * @param    arg
        *           The target to which the data will be copied.
        *
        * @return   <code>true</code> if all processing occurred without error or <code>false</code>
        *           if further outbound processing should be silently (and immediately) aborted.
        */
       private static boolean copyDataOut(String         type,
                                          Class<?>       cls,
                                          boolean        ret,
                                          BaseNativeType bnt,
                                          Object         arg)
       {
          boolean array = cls.isArray();
          boolean ok    = true;

          if (array)
          {
             Class<?> real = cls.getComponentType();
             Object[] args = (Object[]) arg;

             NativeTypeArray  nta = (NativeTypeArray) bnt;
             BaseNativeType[] elements = nta.getElements();

             for (int i = 0; i < args.length; i++)
             {
                if (!copyDataOutWorker(type, real, false, elements[i], args[i], true))
                {
                   ok = false;
                   break;
                }
             }
          }
          else
          {
             ok = copyDataOutWorker(type, cls, ret, bnt, arg, false);
          }

          return ok;
       }

       /**
        * Copy the data back into the given argument from the container for transport to the client.
        *
        * @param    type
        *           The library function's parameter type as defined in the DEFINE PARAMETER
        *           statement.
        * @param    cls
        *           The class of the argument.
        * @param    ret
        *           <code>true</code> if this is a RETURN parameter.
        * @param    bnt
        *           The descriptor from which to copy.
        * @param    arg
        *           The target to which the data will be copied.
        * @param    array
        *           <code>true</code> if this is related to the processing of an element in an
        *           array, <code>false</code> for a scalar variable.
        *
        * @return   <code>true</code> if all processing occurred without error or <code>false</code>
        *           if further outbound processing should be silently (and immediately) aborted.
        */
       private static boolean copyDataOutWorker(String         type,
                                                Class<?>       cls,
                                                boolean        ret,
                                                BaseNativeType bnt,
                                                Object         arg,
                                                boolean        array)
       {
          // certain cases that are expected to copy back are not supposed to, because
          // of other previously determined conditions
          if (!bnt.isCopyOut())
          {
             return true;
          }

          if (cls.equals(integer.class) || cls.equals(int64.class))
          {
             int64         ival = (int64) arg;
             NativeInteger nint = (NativeInteger) bnt;

             // TODO: when used for scalar (non-array) variables, the correct version of this error
             // would look like this:
             // Value <output_value> too large to fit in INTEGER. Line 0 in <called_procedure_name> <calling_program_name>. (15747)
             // But the current generated version will look like this for both scalar and array
             // cases (this is already correct for the array case):
             // Value <output_value> too large to fit in INTEGER. (15747)

             // may raise error 15747
             ival.assign(nint.getValue());
          }
          else if (cls.equals(decimal.class))
          {
             decimal dval = (decimal) arg;

             if ("FLOAT".equals(type))
             {
                NativeFloat nflt = (NativeFloat) bnt;

                // TODO: when the largest 32-bit floating point value (0x7f7fffff) is read into a
                // decimal, the 4GL will raise this error:
                // Insufficient space for float to decimal conversion. (86)
                // There is no good reason for this, since a 32-bit float can hold a value with an
                // exponent of 2^127 which is roughly the same as 10^38.5. In other words, a 32-bit
                // float should have a non-fractional maximum of 39 to 40 digits. Such a value is
                // SUPPOSED to fit into a 4GL decimal. We don't yet know enough about the other
                // possible failing cases to reliably implement this error behavior.

                dval.assign(nflt.getValue());
             }
             else if ("DOUBLE".equals(type))
             {
                NativeDouble ndbl = (NativeDouble) bnt;

                double newval = ndbl.getValue();

                // overflows will normally raise error 536, but when processing decimal array
                // elements the 4GL has a stange behavior where it will not raise an error
                // (regardless of whether NO-ERROR is used), instead it just displays the message
                // (if NO-ERROR is not used) and moves on; it does not abort the copying but of
                // course nothing will be copied to the element being processed in this case
                if (array)
                {
                   // TODO: for some reason it is possible to get messages for errors 86 and even
                   // 15747 (an integer error! but in this case there is no text, just the error
                   // num) in this same array processing; normally these are not possible but they
                   // are here; the problem is we have no idea what actually causes these extra
                   // messages so at this time we won't duplicate this behavior

                   if (dval.isOverflow(new BigDecimal(newval), false, false))
                   {
                      if (!ErrorManager.isSilent())
                      {
                         ErrorManager.displayError(536, "Decimal number is too large");
                      }

                      // don't abort the continued output processing
                      return true;
                   }
                }

                // may raise error 536 in the non-array case (which is OK)
                dval.assign(newval);
             }
          }
          else if (cls.equals(character.class) || cls.equals(longchar.class))
          {
             Text         ctxt = (Text) arg;
             int          clen = TextOps.lengthOf(ctxt);
             NativeString ntxt = (NativeString) bnt;
             String       out  = ntxt.getValue();

             if (out == null)
             {
                // this should not be able to happen from converted 4GL code
                throw new IllegalStateException("NativeString null value was found!");
             }

             if (out.length() > clen)
             {
                // this is a 4GL limitation that we will maintain, even though we can easily
                // copy the data back anyway without any corruption
                String msg = String.format("Your %s buffer has been overrun.  Memory has been " +
                                           "corrupted",
                                           cls.equals(character.class) ? "CHARACTER" : "LONGCHAR");
                ErrorManager.recordOrThrowError(14528, msg, false);
             }
             else
             {
                ctxt.assign(out);
             }
          }
          else if (cls.equals(memptr.class))
          {
             // only return parameters need any processing since all other types are just edits to
             // the already existing buffer (and if there is no existing buffer then it would have
             // failed during inbound processing)
             if (ret)
             {
                memptr       ptr  = (memptr) arg;
                NativeBuffer nbuf = (NativeBuffer) bnt;
                memptr       val  = nbuf.getValue();

                if (val != null && !val.isUnknown())
                {
                   // technically, the 4GL would abend (not a 4GL error, the process actually traps)
                   // if the return memptr passed by the caller was unknown or 0-sized (although
                   // passing an uninitialized memptr works fine); but we will just treat all those
                   // cases the same
                   ptr.setPointerValue(val.getPointerValue());
                }
             }
          }

          return !ErrorManager.isPendingError();
       }

       /**
        * Generate an indeterminate array parameter error.
        */
       private static void genIndeterminateArrayErr()
       {
          String msg = "You cannot pass an indeterminate length array to a DLL as INPUT or receive " +
                       "one back as OUTPUT";
          ErrorManager.recordOrThrowError(12201, msg, false);
       }

       /**
        * Generate an cannot pass an array variable for a RETURN parameter error.
        */
       private static void genNoArrayForReturnErr()
       {
          String msg = "You cannot pass an array variable for the RETURN parameter of a DLL";
          ErrorManager.recordOrThrowError(12270, msg, false);
       }

       /**
        * Generate an character or longchar value cannot be unknown error.
        *
        * @param    force
        *           <code>true</code> to throw the error even in silent mode.
        */
       private static void genCharUnknownErr(boolean force)
       {
          String spec = "A CHARACTER or LONGCHAR variable passed as an OUTPUT parameter to a DLL " +
                        "cannot be the Unknown value or be uninitialised";

          genForcableError(12450, spec, force);
       }

       /**
        * Generate an error that can optionally not be suppressed by NO-ERROR.
        *
        * @param    num
        *           The 4GL error number.
        * @param    spec
        *           The 4GL error text.
        * @param    force
        *           <code>true</code> to throw the error even in silent mode.
        */
       private static void genForcableError(int num, String spec, boolean force)
       {
          try
          {
             ErrorManager.recordOrThrowError(num, spec, false);
          }

          catch (ErrorConditionException err)
          {
             // this should work so long as we are not in warning mode, which is expected to not
             // be active at this time
             if (force)
             {
                err.setForce(true);
             }

             throw err;
          }
       }

       /**
        * Generate an uninitialized MEMPTR error.
        *
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        */
       private static void genUninitMemptrErr(String pname, String iename)
       {
          String spec   = "DLL procedure %s %s using an uninitialised MEMPTR";
          ErrorManager.recordOrThrowError(3233, String.format(spec, iename, pname), false);
       }

       /**
        * Generate a mismatching mode error.
        *
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        */
       private static void genMismatchingModeErr(String pname, String iename)
       {
          String spec   = "Mismatched parameter types passed to procedure %s %s";
          ErrorManager.recordOrThrowError(3230, String.format(spec, iename, pname), false);
       }

       /**
        * Generate a mismatching type error.
        *
        * @param    pname
        *           The legacy name of the external procedure from which this API is being called.
        * @param    iename
        *           The internal-entry name being called which is usually the same as the entry
        *           point name in the library except where an ordinal is being used.
        */
       private static void genMismatchingTypeErr(String pname, String iename)
       {
          String spec   = "Mismatch in the parameter datatypes in DLL procedure %s %s";
          ErrorManager.recordOrThrowError(3231, String.format(spec, iename, pname), false);
       }

       /**
        * Generate a library load error.
        *
        * @param    libname
        *           The name of the library that failed to be loaded.
        * @param    platform
        *           A LibraryManager constant defining the platform on which the client was running.
        */
       private static void genLibraryLoadErr(String libname, int platform)
       {
          String msg = String.format("** Could not load DLL procedure %s. (%d)", libname, 3258);

          if (platform == LibraryManager.PLATFORM_WINDOWS)
          {
             ErrorManager.recordOrThrowError(3258, msg, false, true);
          }
          else
          {
             // one of the rare cases where UNIX/Linux is more complicated than Windows
             String[] msgs = new String[3];

             // we have to manually build the text since the 8013 and 8014 messages don't have a
             // trailing period
             msgs[0] = String.format("** Could not open Dynamic Library: %s (%d)", libname, 8013);
             msgs[1] = String.format("** DLL Error : %s: cannot open shared object file: " +
                                     "No such file or directory (%d)",
                                     libname, 8014);
             msgs[2] = msg;

             ErrorManager.recordOrThrowError(new int[]{ 8013, 8014, 3258 }, msgs, false, true);
          }
       }

       /**
        * Generate a missing entrypoint error.
        *
        * @param    funcname
        *           The name of the function that failed to be loaded.
        * @param    ordinal
        *           The ordinal of the function that failed to be loaded or -1 if no ordinal was
        *           specified.
        */
       private static void genEntrypointErr(String funcname, int ordinal)
       {
          // yes, in the 4GL this funky behavior happens when an ordinal is specified even on
          // non-Windows platforms

          String descr = (ordinal == -1) ? funcname : String.format("%d", ordinal);
          String msg   = String.format("Could not find the entrypoint %s", descr);

          ErrorManager.recordOrThrowError((ordinal == -1) ? 3260 : 3259, msg, false);
       }

       /**
        * Container for context-local data.
        */
       private static class WorkArea
       {
          /** Remote proxy for access to and management of native libraries. */
          private NativeAPICaller libmgr = null;
       }
    }