Using a MEMPTR parameter for a CHAR

I have received many code examples (thank you) but several use parameters of type MEMPTR where a CHARACTER would be more effective, in my opinion.
Since this seems to be a common issue, I will try to explain what's going on.
Most procedure declarations are derived from text in Windows API reference books or helpfiles that are aimed at C programmers. A typical example would be (this is 32-bit but the theory also applies to 16-bit) :

The GetProfileString function retrieves the string associated with the specified key in 
the given section of the WIN.INI file. This function is provided for compatibility with 
16-bit Windows-based applications. Win32-based applications should store initialization 
information in the registry. 
DWORD GetProfileString(
    LPCTSTR  lpAppName,        // points to section name
    LPCTSTR  lpKeyName,        // points to key name
    LPCTSTR  lpDefault,        // points to default string
    LPTSTR   lpReturnedString, // points to destination buffer
    DWORD    nSize,            // size of destination buffer
);   

All these typedefs starting with 'lp' are 'long pointer' to something and the linecomments also clearly say "points to..."
So it is fully understandeble that you would translate this to Progress like this:

PROCEDURE GetProfileStringA EXTERNAL "kernel32" :
  DEFINE INPUT PARAMETER lpAppName        AS MEMPTR.
  DEFINE INPUT PARAMETER lpKeyName        AS MEMPTR.
  DEFINE INPUT PARAMETER lpDefault        AS MEMPTR.
  DEFINE INPUT PARAMETER lpReturnedString AS MEMPTR.
  DEFINE INPUT PARAMETER nSize            AS LONG.
END PROCEDURE.

When you use this function to read a value from an ini file, you probably have character variables (or literals) for the section, key name and default string.And now you have to convert them to and from MEMPTR variables first. So you would have to declare variables of type MEMPTR, allocate them (with set-size) and put the strings in them (with put-string). Then you can call the GetPrivateProfileString procedure. Finally you would have to use get-string to get the answer from lpReturnedString.
To me that looks like a lot of work for passing some strings.

The good news is that it does not have to be so difficult.

A LPCTSTR (or LPTSTR or LPSTR et cetera) is a long pointer that points to the memory location where the first character of a string is stored and subsequent memory locations are occupied by the subsequent characters of the string bla bla bla...

In other words: a LPCTSTR simply points to a string.

A Progress character variable is actually implemented as a memory pointer that points to a string.
As you see, a Progress character variable actually IS compatible with those LPCTSTR-like typedefs! Knowing this, the next procedure definition is valid:

PROCEDURE GetProfileStringA EXTERNAL "kernel32" :
  DEFINE INPUT  PARAMETER lpAppName        AS CHARACTER.
  DEFINE INPUT  PARAMETER lpKeyName        AS CHARACTER.
  DEFINE INPUT  PARAMETER lpDefault        AS CHARACTER.
  DEFINE OUTPUT PARAMETER lpReturnedString AS CHARACTER.
  DEFINE INPUT  PARAMETER nSize            AS LONG.
END PROCEDURE.

As you see, you can now simply call this procedure with Progess character variables. No conversions are needed.
Actually, there is one thing to remember: Windows will NOT allocate memory for the lpReturnedString so you will have to do that yourself. The nSize parameter tells Windows how much memory you have allocated, so it will not write more data than nSize into the lpReturnedString. If you would provide a value for nSize larger than length(lpReturnedString), you might get to see a General Protection Failure because Windows might try to write past the end of your string.
Allocating memory is simply done with the FILL statement.
Here's an example of how to do it:

DEFINE VARIABLE Printername AS CHARACTER NO-UNDO.
 
Printername = FILL(" ", 100). /* = allocate memory for 100 chars */
RUN GetProfileStringA("windows",
                      "device",
                      "-unknown-,",
                      OUTPUT Printername,
                      LENGTH(Printername)).
printername = ENTRY(1,printername).

Input or Output?

The "C" in LPCTSTR tells us this is a constant; the DLL will not modify the contents of this parameter. You can translate it to DEFINE INPUT PARAMETER ... AS CHAR.

LPSTR and LPTSTR parameters (without a "C") are no constants; their contents will be modified by the DLL so these will typically be translated to OUTPUT or INPUT-OUTPUT parameters.

Get rid of the terminating null

API functions return null-terminated strings, that is: a couple of relevant characters terminated by CHR(0) and possibly followed by random characters. This may (or will) cause problems especially if you use the returned string to be concatenated with a second string and send the result to another C-function.
For example: suppose you want to create a temporary file and call function GetTempPathA to get the name of the temp-directory in variable chTempdir. You decide the tempfile should be named chTempfile = chTempdir + "\myfile.tmp" and use this as input parameter to some other C-function. The C-funtion will not process the "\myfile.tmp" part because it only reads up to the CHR(0) character.
So how to deal with this terminating null? Well, some functions tell you where the null is, others don't. For example, GetTempPathA returns the length of the relevant string so you can use this value to trim the result:

DEFINE VARIABLE chTempPath AS CHARACTER NO-UNDO.
DEFINE VARIABLE ReturnedLength AS INTEGER NO-UNDO.
chTempPath = FILL(" ", MAX_PATH). /* = 260 */
RUN GetTempPathA( LENGTH(chTempPath),
                  OUTPUT chTempPath,
                  OUTPUT ReturnedLength).
IF ReturnedLength>0 AND ReturnedLength<=MAX_PATH THEN
   chTempPath = SUBSTRING(chTempPath,1,ReturnedLength).

/* 
Some other functions do not tell you the length of the returned string. In that case you can safely use ENTRY(1,identifier,CHR(0)) like in this code snippet:   RUN gethostname (OUTPUT w-TcpName,
                   INPUT  w-Length,
                   OUTPUT w-Return).
*/
 
  /* Check for errors */
  IF w-Return NE 0 THEN DO:
    MESSAGE "Error getting tcp name." VIEW-AS ALERT-BOX.
    RUN WSACleanup (OUTPUT w-Return).
    RETURN.
  END.
 
  /* Pass back gathered info */
  /* remember: the string is null-terminated so there is a CHR(0)
               inside w-TcpName. We have to trim it:  */
  p-TcpName = ENTRY(1,w-TcpName,CHR(0)).

Thanks to Joern Winther for the ENTRY(1,identifier,CHR(0)) hint.