Backgrounds

.


How is COM/OLE/ActiveX different from DLL?

The original question was: where do I learn about the differences between DLL and COM/OLE and so on, so I don't confuse one with the other and know the limitations/features of each.

Jeez I dunno... where to start? Please help me out by editing this topic whenever you want to change something!

an introduction

A DLL (Dynamic Link Library) is much like a persistent procedure in Progress. In Progress, the persistent procedure contains one or more internal procedures and one or more functions, private or not. The IP's and UDF's that are not private can be called "from outside", provided that you know the name and the parameters and that you have any clue what kind of action the IP/UDF will perform.
Likewise, a DLL contains one or more functions, published or not. (it is uncommon to have procedures instead of functions). You can load the DLL persistent with LoadLibrary, call the published functions and finally use FreeLibrary to get rid of the persistent DLL. As with a Progress persistent procedure, you need to have some sort of written documentation to find the function names, their parameters and expected behaviour.
DLL's work well although problems may rise - especially in setup, maintenance and documentation.
Setup: where should you install these things - before an application can load a DLL it must first try to locate it. You could put it in a subdirectory of your application, but that is not practical if you have many applications (like Microsoft has) and want to reuse common components. You could put them in a shared directory, like windows/system32, but then you enter the maintenance nightmare.
Maintaining a DLL can become a problem both for the programmer and for the end-user. How can you add a new function to an existing DLL, or even a new parameter to an existing fuction, and still make sure that you don't get in trouble with the user base? Since a single DLL can be used by several different applications, there is no way of forcing all those application vendors to simultaniously create an update that matches with the new DLL version.
Documentation: a DLL is a black box, it does not contain its own documentation. It becomes messy when you have a lot of programmers writing DLL's, especially if the company is so big that there are different cultures between departments and different parameter styles you can't get used to.
So even though DLL work well technically, there was a need to invent a way to have DLL's document themselves, publish their file locations to the apps that needed to use them, and to be somewhat version independent.
An ActiveX control is basically just a DLL with some extra functions in it. One of those functions is RegisterServer, which writes the location of the DLL in the registry database under some unique identifier. A calling application does not need to know beforehand where the DLL is located, it only needs to know the unique identifier and can then locate the DLL by looking in the registry, before it loads that DLL into memory. (I say DLL but most of these have the extension OCX, although they are really just DLL with extras). So you don't call LoadLibrary directly, you call it through a wrapper function (not sure but I believe it is CoCreateInstance).
An application is not supposed to call any of the built-in functions directly, because of the risk that you call the wrong version of a function. Instead, the app needs to query the supported interfaces first. To make a long story short, the OCX sends the application a table of built-in functions that can be used. The OCX might support several versions but not all versions can be mixed. Suppose for example an OCX that contains functions AddCustomer and DeleteCustomer, and both have a version 1 and a version 2. One application might want to call AddCustomer version 1 and DeleteCustomer version 1, an other app might want to use version 2 of the interface. A mix is not allowed, e.g. AddCustomer version 2 and DeleteCustomer version 1 will have undesired results. So the OCX sends a table of alllowed functions to the app, with functionpointers to the functions that match the version of the calling application. (in reality there is probably only one function that has different versions. In that case, version 2 of the interface just points to the one and only version of the other functions)
An OCX is typically linked to a TLB resource (Type Library) that contains documentation for the interfaces. Not just for human eyes, but also for the calling application that can now automatically validate the number and type of parameters. This also means that parameter types needed to be standarized, which was not the case with bare DLL's.
Responding to events:

For a normal DLL it is pretty difficult to raise an event that can be handled in the calling application. For this to work, the calling application should have defined an exported function and must have told the DLL what the funcionpointer is for this callback function. (Progress 4GL does not support the definition of exported functions, so this does not work for us). ActiveX has several solutions for this problem, the **eventsink** is most commonly used. The application queries not only the interface for functions that can be called in the OCX, but also queries for a different interface to receive a list of expected callback-functions. The TLB plays an important role here. The received table contains a list of functions that the OCX expects to find in the calling application. The calling app can now dynamically create an eventsink (in its runtime module): a block of dynamic functions that do nothing but forward the call (from the OCX) to the event-handler procedure that you have written (in 4GL). Well actually they do a little bit more than that: they also have to take care of parameter translation.
About parameter tranlation and mashalling:

Back to the TLB for a moment. The TLB describes parameters in standarized datatypes. The host application (Progress) may have to transform the bits and bytes of the actual parameter (like, a character string) to whatever structure the OCX expects (like, a BSTRING). For simple types like integer this may be trivial, but for more complex types like datetime, fonts, colorref, arrays or even a logical it may involve some more work. In Progress this is all handled by the runtime module, although not all types are fully supported yet (like the time part in a datettime structure, or a variant array).
Marshalling guards you against hostile pointers. Remember the oldfashioned DLL API: you define a memory pointer in your application and store a bunch of data behind it. You then pass the pointervalue to the DLL, which can now read and even write the data directly. Works like a charm, until mistakes happen - the data segment may become corrupted and the application may crash, or worse. This is undesirable for a client, but even worse for a server process or a background process. OCX protects you by not allowing to pass pointers - this includes also character strings and output parameters. Instead of passing the pointer itself, the data behind the pointer is packed in a protected envelope and transferred to the OCX and back. Of course this is only possible if the structure of whatever the pointer is pointing to, is well-known and standarized.

so how does this affect your Progress source?

Well, for one thing, you cannot pass pointers to an OCX.
what else? I suppose we need to explain what's going on with instance counters and RELEASE OBJECT...
Ok, the story is unfinished, I give up for now. I might return later to write some more... in the meantime you're welcome to take over


How to receive events from an OLE Automation object

by Theo Albers

When you write your own OLE Automation component in C++ (ATL 3.0 of Microsoft Visual Studio 6+) you will experience the problem that Progress doesn't handle the OLE-events. The "OLE COM"-viewer of Progress is able to show proper events and methods, but the 4GL code simply won't be triggered.

In cooperation with Progress I was able to figure out the problem: when Progress registers for event subscription, it needs to be called back using IDispatch. This is in contrast to other clients like Visual Basic or Windows Scripting Host, which implement the event interface. For more information see the Progress knowledge base entry P56004. For more information on IConnectionPointContainer.Advise() see for instance http:builder.com.com/5100-6373-1050003.html and http:www.techvanguards.com/com/concepts/connectionpoints.asp.

I have attached an ATL sample which shows the usage of a simple OLE component in VB, JavaScript and 4GL. When you want to write your own OLE Automation object, take a look at the Advise() code of MsgQueue.cpp. This is the only part that needs modification when your client is a Progress client.


Override this method to add another check for IDispatch when Progress is calling this method!
STDMETHODIMP CMsgQueue::Advise(IUnknown *pUnk, DWORD *pdwCookie)
{
#ifdef DEBUG_PRINT
AfxMessageBox("In Advise()");
#endif
OK. This function is the important one. This is the place
where we store event sink object for future reference.
HRESULT hr = E_UNEXPECTED;

First we need to make sure that our pointers are valid.
if (0 == pUnk) return E_POINTER;

if (0 == pdwCookie) return E_POINTER;
_IMsgQueueEvents *pEvt = 0;
INTEGER type = 0;

hr = pUnk->QueryInterface(__uuidof(_IMsgQueueEvents), (void **)&pEvt);
if (SUCCEEDED(hr))
{
#ifdef DEBUG_PRINT
AfxMessageBox("Advise()--> sink is IMsgQueueEvents");
#endif
type = 1;
}
else Workaround for Progress, which doesn't implement the event source, but simply provides IDispatch
{
#ifdef DEBUG_PRINT
AfxMessageBox("No");
#endif
hr = pUnk->QueryInterface(__uuidof(IDispatch), (void **)&pEvt);
if (SUCCEEDED(hr))
{
#ifdef DEBUG_PRINT
AfxMessageBox("Advise()--> sink is IDispatch (how do we trust this?)");
#endif
type = 2;
}
else
{
#ifdef DEBUG_PRINT
AfxMessageBox("Advise()--> sink is invalid");
#endif
return CONNECT_E_CANNOTCONNECT;
}
}
Lock();
*pdwCookie = m_vec.Add(pUnk);
hr = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT;
Unlock();
if (hr != S_OK)
pUnk->Release();
if (FAILED(hr))
*pdwCookie = 0;
return hr;
}

Attachments

atlqueue.zip : C++ example of working OLE Automation events


Memptr or character parameters?

Here is an alarming e-mail from Brent Wardle to the Peg. (I know one should no copy e-mails from one forum to antoher, but this one really needs attention in the context of this website).
Hi Peg,
We are upgrading from 9.1D to 10.B and I found an issue tonight that you
may or may not know about.
You can no longer define and use a character output parameter for a
windows DLL call.
You do not get an error message during compile but at run time you get:
"You cannot use OUTPUT to return CHAR or LONGCHAR data. Use MEMPTR
Instead (12200).
Error 12200 is not available on KB error code search.
This impacts some of the DLL calls used in things like GetHostName.p
which is/was used in smtpmail.p and other deadline saving
global-shared.com / FFW code.
Calls like:

PROCEDURE gethostname EXTERNAL "wsock32.dll" :
  DEFINE OUTPUT       PARAMETER p-Hostname      AS CHARACTER.
  DEFINE INPUT        PARAMETER p-Length        AS LONG.
  DEFINE RETURN       PARAMETER p-Return        AS LONG.
END PROCEDURE.

Cause the error.
p-Hostname must now be a MEMPTR and must be sized before and cleaned up
after the call.
Not sure if this impacts Unix/Linux shared lib calls.


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.