Navigation  without Java Scripts

Constructing ActiveX [OLE] Components

By Brad Perkins (bradley@gvn.net)

Aug 16 1997:

 

  1. Added a section on creating a VB5 ActiveX control using a VP 32 bit DLL.
  2. Fixed/Updated Document to reflect VPs newest types and calling conventions.
  3. Supplied a complete example

Introduction

These notes assume the reader has a good grasp of VIP programming including mixed language (using C or C++ with VP) and has some Windows API and OLE/COM experience.

Optionally (If you prefer VB) I have added a large section on creating VB5 ActiveX controls using VIP DLLs. You should (but don’t have to) have access to the Internet. These notes apply only to the development of 32 bit OCX's using Microsoft VC++ 4.x, VC++ 5.0 and VB 5.0. I have not tried this with 16 Bit OCX's or using other Compilers like Borland C++. To date, Microsoft provides the best tools for relatively easy ActiveX /COM development. I'm excited that this works because it gives brand new life to old Prolog code and will reduce the development time for new components. You will still need to learn enough about ActiveX/COM and C++ to connect everything together. With VB you can get by with much less knowledge

There are short code segments from one of my own projects. These aren’t necessarily complete, as I wanted them to be simple and avoid confusion. This document does not cover visual or graphical controls. The sample project I used is an invisible control.

This is by no stretch of the imagination, a tutorial on VIP, VC++ or ActiveX /OLE. Suggested sources for that type of reading or Web resources follows.

 

Other Resources and Suggestions

One of the largest and most valuable resources of information on Visual Basic, Visual C++, COM/DCOM and ActiveX is of course, Microsoft’s Web Site at: http://www.microsoft.com. I think I spend more time there than anywhere else, It’s Gigantic; and always changing, growing and being updated. There are hundreds of documents, dozens of free tools, SDKs and Betas to be had there.

A list of links to documents, software and resources: http://www.microsoft.com/workshop/

Various downloads including ActiveX SDK, ActiveX Control Pad, Web Server software; At the same address select downloads.

If you need a good test container for your new ActiveX controls you can download MS Internet Explorer 3.x for free from the same download page mentioned above.

Visual basic 4.0 or 5.0.

Prolog to ActiveX In 1000 easy steps.

Just kidding. I'll just outline the steps I took to build a control.

ActiveX Options

Option 1.

This document deals with using MSVC++ with Visual Prolog (previously Turbo Prolog, PDC Prolog). You must have Visual C++ version 4.0 higher. Earlier versions may work but I haven’t tested them. You also must have Visual Prolog 4.0 or higher. The benefits of this method are:

Compact and fast executing control.
Access to MS Foundation Classes
Finer programmatic control

Disadvantages are:

Longer development cycle
Linking Prolog with C++ can be problematic.

Option 2.

You could also use Microsoft’s new Visual Basic Control Creation Edition (VB5) which is available from Microsoft’s web site at: http://www.microsoft.com/vbasic/controls/. Using Visual Basic would require you compile your Prolog code into a separate DLL and then declaring and calling the functions from VB5. I haven’t tried this option so this document does not address it. The benefits of this option are:

Faster development cycle
Nice Control Wizards
Microsoft’s commitment to Making VB a standard
You can distribute your code in a separate DLL.

Disadvantages are:

a). Slightly slower execution.

b). Large runtime requirements

 

First Things First

Determine what you want to expose to the outside world in the way of Properties, Methods and Events.

For properties, write predicates that can be called from C++ to Get and Set these properties. For example:

Constants

PropName1 = 10
PropName2 = 11
PropNameN = ...

Global Domains

Value = String

Global Predicates

% Property values can any domain type but in this example
% I've made them strings since strings can easily be converted into any
% base domain

SetMyProp(hInstance, PropName, Value) - (i,i,i) language c
GetMyProp(hInstance,PropName,Value) - (i,i,o) language c

Clauses

% As an example I store my properties in an internal dB
% I have left out safe calls for simplicity

SetMyProp(hInstance,Property,Value):-
        retractall(storeprop(hInstance,Property,_)),
        assert(storeprop(hInstance,Property,Value)).

GetMyProp(hInstance,Property,Value):-
        storeprop(hInstance, Property,Value),!.

Do the same for methods and events. The difference for events is that these are called from Prolog to the C++ side, so there will be no associated Clauses for these declarations. I will discuss events later because they present a special problem from the C++ side.

 

Initializing Prolog

Prolog must be initialized from your custom controls InitCtrlApp::InitInstance member function:

// declare external
extern "C" BOOL InitProlog(void);
///////////////////////////////////////////////////////////////////////////
// CIbisPlus95App::InitInstance - DLL initialization

BOOL CIbisPlus95App::InitInstance()
{

        BOOL bInit = COleControlModule::InitInstance();
       
if (bInit)
{

        // Start Prolog
        InitProlog();
}

        return bInit;
}

It calls the following 'C' function to initialize Prolog. This function can be found in InitPDC32.C and is modified code from the cmain.c file in your foreign directory. The associated .OBJ file must be linked into your OCX projects.

// Initialize Prolog

BOOL InitProlog(void)
{

        BOOL result = FALSE;
        PEXCEPTION_POINTERS lpExc;
        ULONG excCode = 0;

                __try {
                /* Initialize Prolog system */
                RUN_Init(&PROLOG_Vars,
                        &PROLOG_SymbolTable,
                        &PROLOG_ModTab,
                        0,
                        FALSE,
                        FALSE);

}

__except ( lpExc = GetExceptionInformation() ) {
        excCode = (lpExc->ExceptionRecord)->ExceptionCode;
                }
                ExceptionHandler( excCode );
return(result); }

 

Define a Global predicate to terminate an instance of the Control. It'll need at least one argument (to pass the OCX Instance handle). Any other arguments that your Prolog code will need (Window handles or whatever) should also be included. In my example OCX I clean up any internal database stuff, deleting any references to the OCX Instance that is being unloaded. VP will need to pass the OCX Instance handle back to C++ when Events are fired. In some controls you may need to initialize and then cleanup code or data on each instance of a controls creation or destruction. This can be accomplished from the controls Constructor and Destructor. In this case I need to cleanup as each instance is destroyed.

Create an ExitInstance member function for the control module:

 

// overrides ….
// new ExitIntance Member

virtual void ExitInstance(CIbisPlus32Ctrl*)

this is called from the controls destructor:

/////////////////////////////////////////////////////////////////////////////
// CIbisPlus32Ctrl::~CIbisPlus32Ctrl - Destructor

CIbisPlus32Ctrl::~CIbisPlus32Ctrl()
{

ExitInstance(this);

}

And then create the member function:

// C++ Declaration for InstanceEXIT
// This calls the Prolog InstanceEXIT function
// Of course you could bypass much of this and just call the Prolog
// Function directly from the destructor

extern "C" void InstanceEXIT(COleControl*);

/////////////////////////////////////////////////////////////////////////////
// Exit an instance of the control
// This calls The Prolog InstanceEXIT predicate

void CIbisPlus32Ctrl::ExitInstance(CIbisPlus32Ctrl* pControl)
{

InstanceEXIT(pControl);
}

 

From Prolog clean up anything associated with the instance that’s being shut down:

% clear_all deletes all internal database stuff associated with this instance
InstanceEXIT(OcxHnd):-
        clear_all(OcxHnd),!.

 

You need to terminate Prolog when the last instance of your control is unloaded. This is done from the CntlApp::ExitInstance member function:

// Declarations
extern "C" void ExitProlog(void);

////////////////////////////////////////////////////////////////////////////
// CIbisPlus32App::ExitInstance - DLL termination

int CIbisPlus32App::ExitInstance()
{

        // Terminate prolog
        ExitProlog();
        return COleControlModule::ExitInstance();
}

Which calls this function in initPDC32.C:

// Call This From CntlExitApp
// To Exit Prolog
void ExitProlog(void) {
        RUN_End(FALSE,FALSE);
}

 

Create A Skeleton ActiveX

Build an OCX skeleton using MSC++ 4.0 OLE Control wizard. Include all the methods, properties and events that you have predetermined for your Prolog project. Compile, link (without linking in Prolog) and load into the test container if you want before going any further. This will assure you that the Class Wizard has done it's job (Because it DOES have bugs, at least in version 4.0).

Do The Hard Part

Define all of your extern "C" functions for your Prolog Predicates (methods and property SetProperty/GetProperty) and link in your modules, including the PrologINIT module. Work this until there are no undefined externals or LIB errors before going on (See the compiler/Linker options sections later in this doc). To prevent function name mangling (also called decorating) by C++, external declarations on the C++ side must use the extern "C" directive:

// Declarations in the control

extern "C" int mPrologMethod(COleControl*,LPCTSTR);
extern "C" LPCTSTR mGetProperty(COleControl*,short);
extern "C" void mSetProperty(COleControl*,short,LPCTSTR);

// this is our Prolog Init function declaration and should include any other args you may
// need. It, in turn calls PrologINIT.

extern "C" BOOL InitProlog();

 

// Declare your Property Constant names; Make sure they match your Prolog property IDs

const short PROPNAME1 = 10;
const short PROPNAME2 = 11;
const short PROPNAME... = Next ID#;

// Set Prolog properties
LPCTSTR mGetProperty(DWORD OcxInstance,short propname)
{

//BOOL result = FALSE;
PEXCEPTION_POINTERS lpExc;
LPSTR FAR lpszValue;
ULONG excCode = 0;
        __try {
        unsigned SaveSP = GETSP;

        if(RUN_StackTrap()){

/* Catch Exits Here */

        if((!RUN_StackTrap()) && RUN_ErrorFlag) { /* Avoid double error's */

        PROLOG_ErrReport(RUN_ExitCode); /* Normal Prolog Error */

        }

        }

        else if(!RUN_StackBTrack()) {
                /* Call Prolog Goals Here */
        GetPrologProperty(OcxInstance,propname,&lpszValue);

        }

        RUN_RemoveTrap(SaveSP); /* Remove The Trap */
        return(lpszValue);

}

__except ( lpExc = GetExceptionInformation() ) {
        excCode = (lpExc->ExceptionRecord)->ExceptionCode;
        }
        ExceptionHandler( excCode );
return(NULL);

}

 

// Set Prolog properties
BOOL mSetProperty(DWORD OcxInstance,short prop,LPCTSTR lpszValue)
{
BOOL result = FALSE;
PEXCEPTION_POINTERS lpExc;
ULONG excCode = 0;

                __try {
                unsigned SaveSP = GETSP;

                if(RUN_StackTrap()){
/* Catch Exits Here */
                if((!RUN_StackTrap()) && RUN_ErrorFlag) {
/* Avoid double error's */
                PROLOG_ErrReport(RUN_ExitCode);
/* Normal Prolog Error */

                }

                }

                else if(!RUN_StackBTrack()) {
                        /* Call Prolog Goals Here */
                        SetPrologProperty(OcxInstance,prop,lpszValue);
                }

                RUN_RemoveTrap(SaveSP); /* Remove The Trap */
                        return(TRUE);

                }
__except ( lpExc = GetExceptionInformation() ) {
        excCode = (lpExc->ExceptionRecord)->ExceptionCode;

        }
        ExceptionHandler( excCode );

return(result);

}

 

 

// Execute Prolog Method Code
short mPrologMethod(DWORD OcxInstance,LPCTSTR lpszExec)

{

int result = 0;
PEXCEPTION_POINTERS lpExc;
ULONG excCode = 0;

        __try {
        unsigned SaveSP = GETSP;

        if(RUN_StackTrap()){ /* Catch Exits Here */
        if((!RUN_StackTrap()) && RUN_ErrorFlag) { /* Avoid double error's */
        PROLOG_ErrReport(RUN_ExitCode); /* Normal Prolog Error */

        }

        }

        else if(!RUN_StackBTrack()) {
        /* Call Prolog Goals Here */
        result = PrologMethod(OcxInstance,lpszExec);

}

RUN_RemoveTrap(SaveSP); /* Remove The Trap */
        return(result);

}

__except ( lpExc = GetExceptionInformation() ) {
        excCode = (lpExc->ExceptionRecord)->ExceptionCode;

        }

        ExceptionHandler( excCode );

return(result);

}

 

Easy Properties and Methods

Declare all of your Property/Method Functions in the ActiveX Control (Events are covered near the end of this doc):

extern "C" int mPrologMethod(COleControl*,LPCTSTR);
extern "C" LPCTSTR mGetProperty(COleControl*,short);
extern "C" void mSetProperty(COleControl*,short,LPCTSTR);

 

A Sample Property From My Control

Cache is a read-only property that returns a string. IBIS_CACHE is a integer constant that identifies the property on both [C++ & Prolog] sides. Some properties should be read-only that is, the user should not be able to set them to a value. The following C++ code for CIbisPlus32Ctrl::SetCache shows how to throw a runtime error informing the user that the property can’t be set.

 

// constant value for retrieving memory cache
const short IBIS_CACHE= 23;

BSTR CIbisPlus32Ctrl::GetCache()

{

        CString strResult;
        strResult = mGetProperty(this,IBIS_CACHE);
        return strResult.AllocSysString();

}

// warn the programmer that this is a read-only property
void CIbisPlus32Ctrl::SetCache(LPCTSTR lpszNewValue)

{

        ThrowError(CTL_E_SETNOTSUPPORTEDATRUNTIME,

                "Cache is a read-only property.");

}

Here is the prolog code that retrieves the IBIS-Plus cache memory string and returns it to the control:

CONSTANTS
cache = 23

GLOBAL DOMAINS
OCX = DWORD

GLOBAL PREDICATES
GetPrologProperty(OCX,Integer,String) - (i,i,o) language c
% Called By Control as Methods
PrologMethod(OCX,STRING) - (i,i) language c
SetPrologProperty(OCX,Integer,String) - (i,i,i) language c

 

 

CLAUSES

% this is general purpose property retrieve clause that returns any property
GetPrologProperty(OcxHnd,Item,Data):-
        trap(GetData(OcxHnd,Item,Data),_,error_handler(OcxHnd,"GetProperty")),!.
GetPrologProperty(_,_,"").

% cache is the property constant
% the _Cache pred returns a formatted string list of IBIS Plus memory cache
GetData(OcxHnd,cache,String):-_Cache(OcxHnd,String),!.

Executing methods is virtually the same as setting properties. In this case I just send a string to prolog for interpreting:

 

BOOL CIbisPlus32Ctrl::ExecuteIBIS(LPCTSTR ExecString)

{

mPrologMethod(this,ExecString);
return TRUE;

}

 

Handling Events

Events are sent from Prolog to the control and are handled differently. The should be declared and implemented in OLECtrl. This example is a simple error handler event. The event contains an error number and string describing the error.

 

extern "C" void ErrorEvent(CIbisPlus32Ctrl *pControl,short ErrorCode,LPCTSTR ErrorText)

{

// Error events
pControl->FireErrorEvent(ErrorCode,ErrorText);

}

 

The Prolog error predicate reads the error file and formats the error text before firing the event.

In your globals module:

GLOBAL PREDICATES
ErrorEvent(OCX,SHORT, STRING) - (i,i,i) language c % ErrorCode, Text

 

In your code module:

PREDICATES
get_error_file_text(OCX,Integer,String)

CLAUSES
error_handler(OcxHnd,WndProcName) :- % )
        lasterror(ErrorNo,FileName,_,Position),
        get_error_file_text(OcxHnd,ErrorNo,ErrText),
        format(STR,"EXIT! in predicate: %\nError:%\nFile:%\nPos: %\nDescription: %",
        WndProcName, ErrorNo, FileName, Position,ErrText),
        % Call Controls Error Event directly
        ErrorEvent(OcxHnd,internalerror,STR),!.

 

Using VIP DLLs with VC++ To Create Controls

You can use new or existing VIP DLLs [or DLLs in any language] in conjunction with VC++ to create ActiveX controls. There are a few advantages in doing so:

You can distribute the DLL alone to those who don’t use ActiveX.
There is no need to initialize and shut down Prolog from your VC++ code.
There are no problems associated with compiling and linking mixed language code.
You can encapsulate low level functions into higher level, easier to use methods.
You could wrap multiple DLLs into a single ActiveX control.

There are only a few things to consider. To simplify things use the following C++ definitions to obviate the need for external declarations in a .DEF file or .LIB file to resolve the externals in the DLL at link time.

#define DllExport __declspec( dllexport )
#define DllImport __declspec( dllimport )

Note1: You only need "DllExport" if you are calling a function in C++ from the DLL. For instance, to fire an event from Prolog. I try to avoid this because it makes the DLL less useful in some containers. I try to handle events by evaluating returned results from functions.

Note2: The sample code here is from a different project (A document parser) where I created a DLL in VIP.

Now define the DLL functions you wish to expose to C++.

extern "C" DllExport void traceit(LPCTSTR); // Call DEBUG TRACE from Prolog
extern "C" DllImport BOOL parser_ADDWORD(LPCTSTR,LPCTSTR,LPCTSTR); // Section, Field, Synonym
extern "C" DllImport BOOL parser_INSERTWORD(LPCTSTR,LPCTSTR,LPCTSTR); // Section, Field, Synonym
extern "C" DllImport BOOL parser_DELWORD(LPCTSTR,LPCTSTR,LPCTSTR); // Section, Field, Synonym
extern "C" DllImport BOOL parser_FINDMATCH(LPCTSTR,LPCTSTR,LPSTR FAR *,LPSTR FAR * ,short FAR *,LONG FAR *,LONG FAR *,short FAR *); // - (i,i,i,i,o,o,o,o)
extern "C" DllImport BOOL parser_PARSE(LPCTSTR); // Document Text
extern "C" DllImport BOOL parser_FRONTTOKEN(LPCTSTR); // True if fronttoken
extern "C" DllImport BOOL parser_DICTIONARY(void); // Is there a dictionary
extern "C" DllImport BOOL parser_ADJACENT(short,LONG,short FAR* Xpos,short FAR* Len,LPSTR FAR* FoundSect ,LPSTR FAR* FoundField); // - (i,i,o,o,o,o) language c

The DLL should be loaded from OLEApp:InitInstance. It should also have some error trapping in case the DLL is nonexistent or corrupt. I haven’t done the latter in this sample so I’ll scold myself. You can omit this step if you simply include the DLL’s export lib in the project.

////////////////////////////////////////////////////////////////////////////
// CParseSnipApp::InitInstance - DLL initialization

BOOL CParseSnipApp::InitInstance()
        {

        BOOL bInit = COleControlModule::InitInstance();

        if (bInit)

        {

        LoadLibrary("PARSER32.DLL");

        }

        return bInit;

        }

Handling properties, methods and events are handled as if they were just simple C functions. And that’s it! So much easier than mixed language programming [linking the two together].

That pretty much covers all the procedures in implementing ActiveX controls in VIP. If you want to use more complex types; Well, I’ll leave that to you.

 

MSVC++ Compile/Link Settings

This series of screen shots may help you set up the C++ compiler, though settings may vary for your control. This section applies only if you are linking Prolog .OBJs and LIBS with C++’s. I’m only including the settings that may need to be specifically set.

 

 

Struct member alignment must match setting in VIP compiler

 

Make certain to include VIP required OBJs and LIBS and your projects symbol table

 

Resolving A Common MS Linker (LNK2005) Error

The following exerpt is from Microsoft article ID: Q148652. The article provides two solutions. The first is to ensure that all MFC libs are linked before the CRT libs. The second is more involved so I’ll leave it up to you to read the complete article.

When the C Run-Time (CRT) library and MFC libraries are linked in the wrong order, LNK2005 errors like the following may occur:

nafxcwd.lib(afxmem.obj) : error LNK2005:
"void * __cdecl operator new(unsigned int)"(??2@YAPAXI@Z) already
defined in LIBCMTD.lib(new.obj)

nafxcwd.lib(afxmem.obj) : error LNK2005:
"void __cdecl operator delete(void *)"(??3@YAXPAX@Z) already defined
in LIBCMTD.lib(dbgnew.obj)

nafxcwd.lib(afxmem.obj) : error LNK2005:
"void * __cdecl operator new(unsigned int,int,char const *,int)"
(??2@YAPAXIHPBDH@Z) already defined in LIBCMTD.lib(dbgnew.obj)

mfcs40d.lib(dllmodul.obj): error LNK2005: _DllMain@12 already defined in
MSVCRTD.LIB (dllmain.obj)

 

Ignore Libraries may differ for your projects

Most of the other settings are self explanatory or handled by VC++. I welcome any suggestions/fixes. I’ll add or make changes and include your name for the submission. If you have any questions you can email me at: bradley@gvn.net

 

The Same Story In VB 5.0

… only a lot easier.

You can now pass function pointers as arguments from VB. This is quite a change since B.G. said doing stuff like this was impossible (from Basic) just a few years ago. So I took advantage of this "miracle" to call VB events from VIP.

External predicatest to be called by VB must use the "stdcall" calling convention.

% Use Function pointers for VB Events
ERRORCALLBACK = determ (INTEGER err, STRING desc) - (i,i) language stdcall
PARSECOMPLETEEVENT = determ () - language stdcall

VB Types VS VIP Types

ByVal Integer = DWORD
AddressOf = DWORD
Long = Long