Calling DLL routines from LotusScript. Part II: Lotus Notes C API

Download examples in Release 6 NSF format

I originally conceived of writing on this topic 6 years ago when I wrote my Windows API article. Back then, I was using Notes Release 4.6. Amazingly, this topic is still as relevant today as it was back then! This article is not a tutorial on using the C API but only on the nuances of using the C API through LotusScript.

At times, LotusScript falls short in giving you the access you need to your Notes/Domino databases through the classes it provides. On the other hand, you can do theoretically anything to your databases by using the Lotus C API. Although, coding in C (or C++) to get full access to your databases is not for the faint of heart. And, if you are developing for the Notes client, C and C++ programs are not easy to roll out. But, there is a middle ground. You can call some of those same C API routines directly from LotusScript without requiring additional software for deployment. This only works with the C API and not the C++ API since the C++ API requires a DLL that does not install with the base Notes client.

Coding for the C API is analogous to coding for the Windows API. There is one major advantage. Whereas for Windows, you need to hunt around to find the calls implemented inside its DLLs, Lotus has it all documented in one place: the Lotus C API Toolkit for Domino and Notes. Download this toolkit before proceeding because you will need the reference database as your guide to all the calls you can make. It is an excellent source of information.

Example 1: a simple example made simpler
It is probably best to start with an example. Below is a variation of one of the sample programs that comes with the C API toolkit. It returns the major build number of the Notes/Domino executable. It has been further simplified to save space here, but, it still remains functional. Here we are using the local names.nsf as the target database so the major build number will be that of our client. If we wanted to access the names.nsf database on a server, we would instead specify “server!!names.nsf” for db_filename.

Example 1 in C code

/************************************************************************

PROGRAM: getbuild

FILE: getbuild.c

PURPOSE: Get the Domino and Notes major build number and print it
to the screen.

SYNTAX: getbuild

COMMENTS:
This program gets the Domino and Notes major build number using the
Lotus C API for Domino and Notes function NSFDbGetBuildVersion().
This opens the specified database, calls NSFDbGetBuildVersion,
and prints the information on the screen.

**************************************************************************/

/* OS and C include files */

#include <stdio.h>
#include <string.h>

/* Lotus C API for Domino and Notes include files */

#include <global.h>
#include <nsfdb.h>
#include <nsfdata.h>
#include <osmisc.h>

/* Local function prototypes */
void LNPUBLIC ProcessArgs (int argc, char *argv[],
char *db_filename);

void PrintAPIError (STATUS);

#define STRING_LENGTH 256

/* Lotus C API for Domino and Notes subroutine */

int main(int argc, char *argv[])
{

   char      *db_filename;      /* pathname of source database */
   DBHANDLE  db_handle;         /* database handle */
   WORD      wbuild;
   STATUS    error = NOERROR;   /* error code from C API*/
   char      database_name[STRING_LENGTH];

   db_filename = "names.nsf";

   if (error = NotesInitExtended (argc, argv))
   {
      printf("\n Unable to initialize Notes.\n");
      return (1);
   }

   /* Open the database. */

   error = NSFDbOpen (db_filename, &db_handle);
   if (NOERROR != error)
   {
      printf("Error: unable to open database '%s'.\n", db_filename);
      NotesTerm();
      return (1);
   }

   /* Get the major build number. */

   error = NSFDbGetBuildVersion (db_handle, &wbuild);
   if (NOERROR != error)
   {
      NSFDbClose (db_handle);
      NotesTerm();
      return (1);
   }

   printf ("\nThe major build number is: %d\n", wbuild);

   /* Close the database. */

   error = NSFDbClose (db_handle);
   if (NOERROR != error)
   {
      NotesTerm();
      return (1);
   }

   /* End of subroutine. */

   NotesTerm();
   return (0);
}

Now we’ll look at the same functionality but implemented using LotusScript calls to the C API. The first piece to examine is the Declarations section that makes the external functions available to LotusScript. All of the C API functions are implemented in nnotes.dll. For reference, I like to include the C signature as a comment before the Declare statement. It helps when doing the initial translation as well as later for documentation purposes.

Example 1 declarations in Lotusscript code

{declarations)

'STATUS LNPUBLIC NSFDbClose( DBHANDLE hDB);
Declare Function NSFDbClose Lib "nnotes.dll" (Byval hDB As Long) As Integer

'STATUS LNPUBLIC NSFDbOpen(char far *PathName, DBHANDLE far *rethDB);
Declare Function NSFDbOpen Lib "nnotes.dll" (Byval filepath As String, DB As Long) As Integer

'STATUS LNPUBLIC NSFDbGetBuildVersion( DBHANDLE hDB, WORD far *retVersion);
Declare Function NSFDbGetBuildVersion Lib "nnotes.dll" (Byval hDB As Long, retVersion As Integer) As Integer

 

The key is to properly translate the C data types into LotusScript data types. Since you are often dealing with pointers into memory, care should always be taken. You should expect client crashes as you work with C API calls for the first time and work out their declare statements. To do the translation you first need to consider the data type(s) used in the API call definition and determine the correct LotusScript data type. Generally, you are dealing with a well-defined subset of C types that, once mapped the first time, can help you with further mappings. A few of the more common are in the following table:

C language data type In LotusScript declare as
int Long
BYTE Byte
WORD Integer
DWORD Long
HANDLE Long

Once the type translation is determined you then need to determine the type of reference. Like C, LotusScript has two types of references for passed variables: by value (ByVal is specified) and by reference (nothing is specified). In C, variables passed by reference are done via the use of a pointer. When a C API declaration is defined using a pointer, the asterisk (*) symbol will precede the variable name. Variable names without the preceding asterisk are being passed by value.

The following table will help you with some sample translations you will see often. It is not intended to be a complete reference.

C API data type In LotusScript declare as
HANDLE hDb Byval hDb As Long
HANDLE *rethSummary rethSummary As Long
DWORD Flags Byval Flags As Long
DWORD *retNumEntries retNumEntries As Long
DWORD far *retBufferLength retBufferLength As Long
char far *retBuffer Byval retBuffer As String
WORD What Byval What As Integer
WORD far *retVersion retVersion As Integer
BOOL fStripTabs Byval fStripTabs As Integer

With the declarations set, we can get to the code. What follows is an agent’s initialize routine that prints the build message to the status bar using the C API calls. You will notice is that flow is identical since we make the same calls in the same order. You can implement the other samples that come with the C API in a similar way. Instead of names.nsf, the current database is used.

Example 1 in LotusScript code

Sub Initialize
   Dim Bufferstr As String
   Dim ReturnCodel As Long
   Dim hDBl As Long
   Dim buildn As Integer

   Dim session As New NotesSession

   'Open the database
   ReturnCodel = NSFDbOpen(session.CurrentDatabase.FilePath, hDBl)
   If ReturnCodel <> 0 Then
      Error 9999, "An error occurred in the main routine calling " + _
         "the API function NSFDbOpen." & Chr$(10) & + _
         "The return code was " & Trim$(Str$(ReturnCodel)) & "."
      Exit Sub
   End If

   'Get the major build number
   ReturnCodel = NSFDbGetBuildVersion(hDBl, buildn)
   If ReturnCodel <> 0 Then
      Error 9999, "An error occurred in the main routine calling " + _
         "the API function NSFDbGetBuildVersion." & Chr$(10) & + _
         "The return code was " & Trim$(Str$(ReturnCodel)) & "."
      Call NSFDbClose(hDBl)
      Exit Sub
   End If

   Print "The major build number is: " & buildn

   'Close the database
   ReturnCodel = NSFDbClose(hDBl)

End Sub

When using the C API, I have found that it pays to follow the example set in the samples by building in a fair amount of error checking, messaging and recovery. The general rule is to check every operation for an error and, if you find one, don’t continue to the next operation. Instead, generate a meaningful error message and cleanup anything you can before exiting.

Example 2: a more practical example – changing the template name of a database
The next example is a practical one, although, I don't remember exactly why I had to do this at one time. The template name is a property of NotesDatabase, although, it is read-only. If you want to change this programmatically, you can do so by using the C API. This example illustrates a couple of points: the translation of constants that is necessary when moving from C to LotusScript and the allocation of memory when passing data between LotusScript and the C API.

We'll first start with the use of constants. The template name is stored as one of several elements in the "database information buffer." To get the buffer you use NSFDBInfoGet and to put data back into it you use NSFDBSetInfo. The database information buffer you pass in and get out contains several parts. You use NSFDBInfoParse to get a specific piece out of the database information buffer. You do this by specifying a constant as one of the parameters to NSFDBInfoParse that represents the piece you want. From the C API documentation we can see that these constants are in the form INFOPARSE_xxx and are defined in nsfdb.h. Inside this file we find the following C definitions:

Example 2 - C defintions

/* Define argument to NSFDbInfoParse/Modify to manipulate components from DbInfo */

#define INFOPARSE_TITLE 0
#define INFOPARSE_CATEGORIES 1
#define INFOPARSE_CLASS 2
#define INFOPARSE_DESIGN_CLASS 3

For the constant values you will usually see integers, whose values you can translate directly, and hexadecimal numbers. In C, the hex numbers are specified in a form like 0x0100. In LotusScript, you would specify &h0100 for the same value. We can translate the definitions we need for the example along with the other calls and definitions needed for the aforementioned C API calls into the declarations section of a LotusScript agent:

Example 2 - LotusScript declarations

(declarations)

'/* Define argument to NSFDbInfoParse/Modify to manipulate components from DbInfo */
Const INFOPARSE_TITLE = 0
Const INFOPARSE_CATEGORIES = 1
Const INFOPARSE_CLASS = 2
Const INFOPARSE_DESIGN_CLASS = 3

'/* NSF File Information Buffer size. This buffer is defined to contain Text (host format) that is NULL-TERMINATED. This is the ONLY null-terminated field in all of NSF. */
Const NSF_INFO_SIZE = 128

'STATUS LNPUBLIC NSFDbOpen(char far *PathName, DBHANDLE far *rethDB);
Declare Function NSFDbOpen Lib "nnotes.dll" (Byval filepath As String, DB As Long) As Integer

'STATUS LNPUBLIC NSFDbClose( DBHANDLE hDB);
Declare Function NSFDbClose Lib "nnotes.dll" (Byval hDB As Long) As Integer

'STATUS LNPUBLIC NSFDbInfoGet( DBHANDLE hDB, char far *retBuffer);
Declare Function NSFDbInfoGet Lib "nnotes.dll" (Byval hDB As Long, Byval retBuffer As String) As Integer

'void LNPUBLIC NSFDbInfoModify( char far *Info, WORD What, char far *Buffer);
Declare Function NSFDbInfoModify Lib "nnotes.dll" (Byval Info As String, Byval What As Integer, Byval Buffer As String) As Integer

'void LNPUBLIC NSFDbInfoParse( char far *Info, WORD What, char far *Buffer, WORD Length);
Declare Function NSFDbInfoParse Lib "nnotes.dll" (Byval Info As String, Byval What As Integer, Byval Buffer As String, Byval Length As Integer) As Integer

'STATUS LNPUBLIC NSFDbInfoSet( DBHANDLE hDB, char far *Buffer);
Declare Function NSFDbInfoSet Lib "nnotes.dll" (Byval hDB As Long, Byval Buffer As String) As Integer

The next part of this example is how to deal with the information buffer. If you have done much work with the Windows API, you have probably used a similar method to pass information between the two languages. You need allocate memory for the buffer by building a string of the correct size. For this, use the String function. The C API documentation tells us that the size of the buffer is defined by NSF_INFO_SIZE. This value is found in nsfdb.h and has been translated in the declarations above.

Example 2 - LotusScript code

Sub Initialize

   Dim Bufferstr As String
   Dim designBufferstr As String
   Dim ReturnCodel As Long
   Dim hDBl As Long
   Dim session As New NotesSession

   'Open the database
   ReturnCodel = NSFDbOpen(session.CurrentDatabase.FilePath, hDBl)
   If ReturnCodel <> 0 Then
      Error 9999, "An error occurred in the calling the API function " + _
         "NSFDbOpen." & Chr$(10) & "The return code was " & +_
         Trim$(Str$(ReturnCodel)) & "."
      Exit Sub
   End If

   'Reserve some space
   Bufferstr = String$(NSF_INFO_SIZE,0)

   'Get the database information buffer
   ReturnCodel = NSFDbInfoGet(hDBl,Bufferstr)
   If ReturnCodel <> 0 Then
      Error 9999, "An error occurred calling the API function "+_
         "NSFDbInfoGet." & Chr$(10) & "The return code was " & + _
         Trim$(Str$(ReturnCodel)) & "."
      ReturnCodel = NSFDbClose(hDBl)
      Exit Sub
   End If

   'Reserve some space
   designBufferstr = String$(NSF_INFO_SIZE,0)

    'Gets a specified piece of information from the database information buffer
   Call NSFDbInfoParse (Bufferstr, INFOPARSE_DESIGN_CLASS, designBufferstr, NSF_INFO_SIZE)

   Print "Current template used is " + designBufferstr

   'Modify a specified piece of information in the database information buffer
   Call NSFDbInfoModify(Bufferstr, INFOPARSE_DESIGN_CLASS, "New template name")

   'Set the database information buffer
   ReturnCodel = NSFDbInfoSet(hDBl, Bufferstr)
   If ReturnCodel <> 0 Then
      Error 9999, "An error occurred calling the API function " + _
         "NSFDbInfoSet." & Chr$(10) & "The return code was " & + _
         Trim$(Str$(ReturnCodel)) & "."
      ReturnCodel = NSFDbClose(hDBl)
      Exit Sub
   End If

   'Close the database
   ReturnCodel = NSFDbClose(hDBl)
End Sub

Example 3: Using C structures
The last example presented here contains details on using structures contained within the C API. Often structures need to be passed into and retrieved from C API calls. This example will retrieve the user activity information from a database. It is a was chosen because it is a simple example that uses a structure not because it is a practical example. You first start by translating the C struct into a LotusScript Type. For this, you will need to translate the data types from C into LS the same way you do for the function calls. You will find the struct definition in the C API documentation as well as in a header file. Again, as a matter of course, I like to include the C definitions as comments along with the LotusScript code. The structure we are dealing with here is DBACTVITY.

Example 3 - LotusScript declarations

(declarations)

'Structures used by Notes C API
Type TIMEDATE
   Innards(1) As Long 'DWORD
End Type

Const MAXALPHATIMEDATE = 80

Type DBACTIVITY
   First As TIMEDATE
      'TIMEDATE /* Beginning of reporting period */
   Last As TIMEDATE        'TIMEDATE /* End of reporting period */
   Uses As Long            'DWORD /* # of uses in reporting period */
   Reads As Long           'DWORD /* # of reads in reporting period */
   Writes As Long          'DWORD /* # of writes in reporting period */
   PrevDayUses As Long     'DWORD /* # of uses in previous 24 hours */
   PrevDayReads As Long    'DWORD /* # of reads in previous 24 hours */
   PrevDayWrites As Long   'DWORD /* # of writes in previous 24 hours */
   PrevWeekUses As Long    'DWORD /* # of uses in previous week */
   PrevWeekReads As Long   'DWORD /* # of reads in previous week */
   PrevWeekWrites As Long  'DWORD /* # of writes in previous week */
   PrevMonthUses As Long   'DWORD /* # of uses in previous month */
   PrevMonthReads As Long  'DWORD /* # of reads in previous month */
   PrevMonthWrites As Long 'DWORD /* # of writes in previous month */
End Type

'STATUS LNPUBLIC NSFDbGetUserActivity(DBHANDLE hDB, DWORD Flags, DBACTIVITY far *retDbActivity, HANDLE far *rethUserInfo, WORD far *retUserCount);
Declare Function NSFDbGetUserActivity Lib "nnotes.dll" (Byval hDB As Long, Byval Flags As Long, retDbActivity As DBACTIVITY, rethUserInfo As Long, retUserCount As Integer) As Integer

'STATUS LNPUBLIC NSFDbClose( DBHANDLE hDB);
Declare Function NSFDbClose Lib "nnotes.dll" (Byval hDB As Long) As Integer

'STATUS LNPUBLIC NSFDbOpen(char far *PathName, DBHANDLE far *rethDB);
Declare Function NSFDbOpen Lib "nnotes.dll" (Byval filepath As String, DB As Long) As Integer

'STATUS LNPUBLIC ConvertTIMEDATEToText(const void far *IntlFormat, const TFMT far *TextFormat, const TIMEDATE far *InputTime, char far *retTextBuffer, WORD TextBufferLength, WORD far *retTextLength);
Declare Function ConvertTIMEDATEToText Lib "nnotes.dll" (Byval IntlFormat As Integer, Byval TextFormat As Integer, InputTime As TIMEDATE, Byval retTextBuffer As String, Byval TextBufferLength As Integer, retTextLength As Integer) As Integer

The above code section contains all of the declarations we need to make for this example.


Note: This uses a somewhat less flexible way of declaring ConvertTIMEDATEToText. The parameters IntlFormat and TextFormat are not defined using their actual types but using Integer instead. This allows us to pass a 0 in the call which is the equivalent of passing a NULL in the C world. A NULL for these parameters instructs the ConvertTIMEDATEToText to build for itself the Format structures using default information about the internationalization settings. A more flexible way would be to implement this by defining the INTLFORMAT and TFMT structures as Types and by using OSGetIntlSettings to populate the INTLFORMAT structure and/or custom building it for your specific need. For this case, we would declare it as follows:

Example 3 - LotusScript declarations

Declare Function ConvertTIMEDATEToText Lib "nnotes.dll" (IntlFormat As INTLFORMAT, TextFormat As TFMT, InputTime As TIMEDATE, Byval retTextBuffer As String, Byval TextBufferLength As Integer, retTextLength As Integer) As Integer

Furthermore, to make this declaration flexible enough to handle both the passing of the NULLs ans the structures you can declare IntlFormat and TextFormat as Any. Although, this reduces the effectiveness that the LotusScript compiler provides.

The INTLFORMAT and TFMT structure implementations as Types have been omitted here, but, by following this example you should be able to translate them. When I attempted to use this alternative for this example, I was able to populate the IntlFormat structure but I received an error which I could not get around when using it with ConvertTIMEDATEToText. I am sure it is possible with the right declarations.


The rest of the code for this example is as follows. The call to NSFDbGetUserActivity populates the Type with data and we can get to its components it like we would for any LotusScript Type.

Example 3 - LotusScript Code

Sub Initialize

   Dim ReturnCodel As Long
   Dim hDBl As Long
   Dim retDbActivity As DBACTIVITY
   Dim rethUserInfo As Long
   Dim retUserCount As Integer
   Dim Flags As Long

   Dim retTextBuffer As String
   Dim retTextLength As Integer
   Dim BufferSize As Integer

   Dim session As New NotesSession

   'Open the database
   ReturnCodel = NSFDbOpen(session.CurrentDatabase.FilePath, hDBl)
   If ReturnCodel <> 0 Then
      Error 9999, "An error occurred calling the API function " + _
         "NSFDbOpen." & Chr$(10) & "The return code was " & + _
         Trim$(Str$(ReturnCodel)) & "."
      Exit Sub
   End If

   Flags = 0
   ReturnCodel = NSFDbGetUserActivity(hDBl, Flags, retDbActivity, rethUserInfo, retUserCount)
   If ReturnCodel <> 0 Then
      Error 9999, "An error occurred calling the API function " + _          "NSFDbGetUserActivity." & Chr$(10) & "The return code was " & + _
         Trim$(Str$(ReturnCodel)) & "."
      Call NSFDbClose(hDBl)
      Exit Sub
   End If

   retTextBuffer = String$(MAXALPHATIMEDATE + 1,0)
   ReturnCodel = ConvertTIMEDATEToText(0, 0, retDBActivity.First, retTextBuffer, MAXALPHATIMEDATE, retTextLength)
   Print "First access = " + Left(retTextBuffer, retTextLength)

   retTextBuffer = String$(MAXALPHATIMEDATE + 1,0)
   ReturnCodel = ConvertTIMEDATEToText(0, 0, retDBActivity.Last, retTextBuffer, MAXALPHATIMEDATE, retTextLength)
   Print "Last access = " + Left(retTextBuffer, retTextLength)

   Print "Uses = " + Cstr(retDBActivity.Uses)
   Print "Reads = " + Cstr(retDBActivity.Reads)
   Print "Writes =" + Cstr(retDBActivity.Writes)

   'Close the database
   ReturnCodel = NSFDbClose(hDBl)
End Sub

The dates are stored in a TIMEDATE structure. This happens to be the same structure you will use when dealing with replica IDs. To get a readable date, we call ConvertTIMDATEToText passing it the correct component of the DBACTIVITY structure.

Conclusion
My own personal experience is to use this technique to solve problems that are simple in nature but not possible to implement in native LotusScript due to the its (sometimes) restricted view of the world. To be sure, there are some things in the C API that are just not a good idea to be doing through LotusScript. I have given up trying to work with Rich Text using this method.

While the C API can be very unforgiving if you reference memory in the wrong way, with some patience, this can be a really powerful solution to your hardest LotusScript problems. Now, you can do things you never thought possible.

Other resources
There is a book on this topic. I have not read nor seen the book so I cannot provide a review of it. The book is called "LotusScript to Lotus C API Programming Guide".

You can also try the page at NSFTools.com where they have some more informtion on the topic.