Using Buffer Variables

Overview

Buffer variables, as their name imply, is a mechanism to access a piece of memory using a specific structure of fields, each one of a specific data type (integer, string, double etc). In its simplest conception can be seen as something similar of the structured variables in other languages. For example in Pascal we have to define a record type and create variables of this type.

type
 Telephone=record
  Name:String[30];
  Address:String[30];
  Phone:String[30];
 end;

var
 tel:Telephone;

The above Pascal code creates a structure definition with the name Telephone and a variable tel of type Telephone. The user can access parts of tel with the following syntax:

tel.Name:='James Jones';
tel.Address:='No men land';
tel.Phone:='5544332244';

Using the dot character and the field name we can access parts of the tel variable. Other languages offer similar ways for the same thing.

CalcIt language doesn't have types like integer, string etc. Every CalcIt variable accepts whatever it is assigned, numeric or alphanumeric. Internally CalcIt handles the types and hides these detail from the user. This makes the programming with CalcIt language faster and sorter. Many such things are handled in the behalf of the user and he/she do not have to spend time for them.

In the evolution of this program appeared the need to offer the capability to call DLL exported functions. This way CalcIt can be expandable. The user can write its own DLLs to call from CalcIt or can call functionality of many established out there APIs (Application Programming Interface) implemented as DLLs. For example he/she can call functions of the Windows API, WinSock API, ODBC API, Oracle API and many more.

Because all these APIs and exported DLL functions are expressed in interfaces with specific types like integer, char, double etc, CalcIt was not able to access them ignoring this fact and using its proprietary type of variables which is understood only in its scope.

So the Buffer variables invented. Buffer variables open a door to the outside world that uses specific types for every variable or record field or function or parameter. Additionally offer much more functionality and convenience than the record variables in Pascal or C.

A Buffer variable at any moment caries two things: The definition of a structure and the memory will be accessed using this structure. Buffer variables can allocate memory equal to size of the structure they use but also can be configured to point in any valid memory address. A Buffer variable is handled at run time and so it can be reconfigured as many times needed in the same code and to completely different structures or memory addresses or memory allocations.

Buffer variables is the only way to make API calls but have other uses also. Can be used in collaboration with File variables or to access any piece of memory in any possible way.

Contents in this topic

Creating Buffer structure Definitions

To define or redefine a Buffer Variable we need a structure definition. This definition can be incorporated directly in the definition or redefinition of the Buffer Variable or can be defined separately and reused in many different ways in the same code.

To create a structure definition we use the command BUFDEF:

const Telephone=BUFDEF(Name:atCHAR[30],
                       Address:atCHAR[30],
                       Telephone:atCHAR[30]);

The above BUFDEF creates a structure of three fields, Name, Address and Telephone, of type atCHAR[30]. As it is obvious a fields definition is a comma separated list of Fields Name - Type pairs.

If consecutive fields share the same type then we can save space and typing time writing the same in the following way:

const Telephone=BUFDEF(Name,Address,Telephone:atCHAR[30]);

BUFDEF returns an ID which is stored in the local constant Telephone. This is possible because BUFDEF is a compile time only command. Is evaluate at compile time and so the value it returns is known at compile time and can be passed in a local constant. You can use a variable to assign this ID but you cannot use it later in a second BUFDEF statement where the previous can be the type of one of its fields.

The fields can use the following predefined types:

atINTEGER
atINT
Signed integer (4 bytes)
atDWORD Unsigned integer (4 bytes)
atWORD Unsigned integer (2 bytes)
atSHORT Signed integer (2 bytes)
atBYTE Unsigned integer (1 byte)
atPOINTER When a field is intended to keep a 32Bit memory address we use this type for readability reasons. Is an integer of 4 bytes.
atFLOAT Double floating point (8 bytes)
atINT64 64 bit signed integer (8 bytes)
atCHAR Defines a string value. For a string value we need to define its size also. We put this size in brackets following atCHAR  e.g. Name:atCHAR[30]. This defines a string of 30 characters. We can assign a string value up to the defined size to every atCHAR field. In case the assigned value is bigger then it is truncated to fit. If the value is less than the defined size a zero is added at the end to form a null terminated string. Be careful of the fact that you get a null terminated string only in case the assigned value is less than the defined size. CalcIt handles correctly these fields in either case but an API call routine that access the field and expects a null terminated value will fail. See about API call using Buffer variables later in this topic.
atCALLBACK Denotes a function pointer parameter. Is an integer of 4 bytes. We use this type for readability reasons.
atBUFFER Using this we can define a field that has the structure defined in another BUFDEF statement. Customer:atBUFFER(CustomerTypeID), where CustomerTypeID is an ID returned by another, previous declared BUFDEF statement.

In the above BUFDEF definition every field caries a single value but can also be an array. Look the following syntax:

const Months=BUFDEF(Name:atCHAR[20](12),
                    Days:atINTEGER(12));

The above BUFDEF defines two fields, Name and Days, with 12 elements each.

Additionally we can define a "structured" field directly in the fields list (instead of using atBUFFER and another BUFDEF definition) as the example below demonstrates (field Money):

const Person=BUFDEF(Name,Address,Telephone:atCHAR[30],Money:(Income,OutCome:atINT));

At many cases we need a structure definition where a group of fields share the same memory address and consequently share the same memory space. Such a capability is found in almost all languages and CalcIt language supports it using the UNION keyword:

const DataType=BUFDEF(
                      Type:atINTEGER,
                      UNION(
                            s:atCHAR[20],
                            n:atFLOAT,
                            i:atINTEGER,
                            b:atBYTE
                           )
);

In the above example fields s, n, i, b share the same memory space. The whole union group (enclosed in parentheses) has a memory size equal to the size of the bigger field (s in our example equal to 20). Changing the value of one of these fields we affect the value of the others. So in normal use we access only one of these fields at a time usually according to the value of a field outside the union group. In the example should be the field Type.

UNION groups can be nested in any level and more than one UNION groups can be created in the same BUFDEF statement.

Creating Buffer Variables

When a BUFDEF ID is available we can use it to define or redefine a Buffer variable:

set tel=BUFFER(Telephone);

When a buffer variable is defined this way, memory is allocated for the size of the structure passed. This memory allocation is initialized by zeroes. As we will see later there is a way to make a Buffer variable, instead of allocating its own memory, to point to any other valid memory address.

In the above way we need two statements to create a Buffer variable. One to create the structure definition and one for the variable creation itself. We can merge these two steps in one with the following syntax:

set tel=BUFDEF(Name:atCHAR[30],
               Address:atCHAR[30],
               Telephone:atCHAR[30]);

The only disadvantage of the above syntax is that we cannot reuse the structure definition to create another Buffer Variable of this type.

Accessing Buffer Variable fields

To access the fields of a Buffer variable is a simple mater. We use the dot character as in most other languages:

tel.Name:='James Jones';

In a case of an array field we have to provide an index in parentheses as usual:

set m=BUFDEF(Name:atCHAR[20](12),
             Days:atINTEGER(12));

m.Name(2):='February';
m.Days(2):=28;

Indexing of array fields is zero based. The first element in the array is at index 0.

Additionally all special case operators (+=, -=, ++, --, &=) can be applied in Buffer variable fields. 

A Buffer variable can be redefined many times in a code, every time using a different structure definition. Because of that compiler cannot decide if the field Name in the above example is correct according the structure assigned at that moment. If it is not correct an error will be generated at run time when the field will be evaluated and validated against the structure that is assigned at this time of code execution. So be careful about this fact.

NOTE: The assignment operator can be used to copy a buffer to another of equal size. Additionally the low level command MovMem can be useful to some purposes some times.

Initializing Buffer variables

We can initialize a Buffer variable field by field using the assignment operator as it is described above. There is another way, also,  which is convenient in many cases: Using operator = and an array expression:

set tel=BUFDEF(Name,
               Address,
               Telephone:atCHAR[30])=['James Jones','No men Land','334455677'];

The above code initializes the fields of Buffer variable tel sequentially using data from the array expression (in this case an inline array) after symbol =. In case of array fields, first all the element of the array are initialized before we go to the next field.

We can also initialize a Buffer variable as a whole or one of each fields using BufferFill command. BufferFill, fills the memory occupied by a Buffer variable or one of each fields with a specific byte value.

BufferFill(bf.Name,0);
BufferFill(bf,0);

Accessing Memory

As we have already see Buffer variables can allocate their own memory. This is not the only case. A Buffer variable, instead of allocating its own memory, can point to any other valid memory address. Where is it possible to get this memory address? 

  1. From an API call that returns a memory address
  2. The address of another Buffer variable or the address of a field of another Buffer variable. We can get such address using the "Address Of" operator (&) as we will see later.
  3. From a File variable using again the "Address Of" operator.

We can make a Buffer variable to point in any valid memory address using a special extension of the Buffer variable definition or redefinition syntax using the ADDR keyword:

set m=BUFDEF(...)ADDR(MemoryAddress);
set m=BUFFER(BufDefID)ADDR(MemoryAddress);

MemoryAddress is of course a 32bit integer value. In case we need the address of a field in another Buffer variable or in a position in a File variable, we use the "Address Of" operator (&).

set m=BUFDEF(...)ADDR(&bf.Name);
set m=BUFDEF(...)ADDR(&FileVar(100));

Using this syntax we are able to see a piece of memory in many "alternative" ways.

Calculating sizes

Using command Size over BUFDEF IDs or Buffer variables or Buffer variable fields, we can find their sizes.

TheSizeIs:=size(BufDefId);
TheSizeIs:=size(bf.Name);
TheSizeIs:=size(bf);

Printing on screen

The whole contents of a Buffer variable can be printed on screen using PrintBuf command. e.g.

set bf=BUFDEF(a,b,c:atINT,x:atCHAR[20])=[10,20,30,'Computer'];
PrintBuf(bf);

This will print:

A = 10
B = 20
C = 30
X = Computer

Importing DLLs and APIs

As we have already said the first reason to introduce Buffer variables in CalcIt was the need to offer the capability to call exported functions in DLLs. It is possible to write a DLL in any favorite language and use it in CalcIt but also we can call DLLs that contain important APIs, like ODBC API, Windows API, WinSock API and many others, adding in CalcIt the capability to handle databases, networks or whatever else is found in a computer.

Buffer variables are used to describe the external function call but also carry all needed information for a successful call. Lets see how with an example:

Lets say that there is a DLL with the name MATH.DLL. This DLL contains many exported functions and one of them has the following interface in C:

int AddTwoNumbers(int num1,int num2);

In a C code a call to this function will be the following:

Summary=AddTwoNumbers(100,200);

The function AddTwoNumbers, as its name implies, adds two numbers (in our case 100 and 200) and returns the result in the variable Summary (=300).

To make a call to this function in CalcIt we create a Buffer variable with the same name as the exported function and a structure equivalent to its parameters interface:

set AddTwoNumbers=BUFDEF(num1,num2:atINTEGER)API('MATH.DLL');

As we can see this syntax uses an extension via API keyword. This extension is used to define the DLL where the exported function resides. The API extension has two additional optional parameters, one to define the type returned by the function and one to define the calling conventions (Windows API or C). By default the returned type is assumed to be atINT and the calling conventions to Windows API. Because our function returns atINT and we assume Windows API calling convention (as it is in most cases) we can safely omit these two parameters.

To make the call we need to use a special command (APICall) and provide the values of the parameters of the function (100 and 200). We have two ways to do that. Lets see the one that looks more space consuming:

AddTwoNumbers.num1:=100;
AddTwoNumbers.num2:=200;
v:=APICall AddTwoNumbers;

The above code enters the two values in Buffer variable AddTwoNumbers in the expected way. Then using command APICall and via a Buffer variable as it appears in the code, we actually make the call. The returned value is assigned in variable v which is a normal CalcIt variable.

There is a sorter way to write the same in a way that more closely resembles a normal function call:

v:=APICall AddTwoNumbers([100,200]);

In the above syntax the Buffer variable is initialized by an array expression before the actual call.

NOTE: APICall command always returns a value even the called API routine it doesn't. In either case if we want to ignore the returned value we can use the command NORET which is offered as a convenience in such occasions.

noret APICall DisplayNumbers([100,200]);

How to call an API routine using a different name for the Buffer variable

There are times that we cannot create a Buffer variable having the same name with an API routine. This is the case when the API routine's name is already reserved by CalcIt. Also there are times that we prefer to declare a different name for the Buffer variable and actually make a call to an API routine that has another. In such cases we use the USE extension keyword of the BUFFER definition syntax. This keyword follows API keyword and in parentheses takes a value which provides the name of the API call.

set selecta=BUFDEF(a1,a2,a3:atINT)API('MYDLL')USE('select'); //select is reserved word
v:=APICall selecta([10,20,30]);
//select of MYDLL is called

When API call address is already known

There are cases where the API call address is known. For example using GetWindowLong Windows API function we can get the 32 bit address of a Window procedure. To call this routine is not possible to use the previous described way. In such a case the CALLAT extension keyword is used (instead of keyword API):

set AnyName=BUFDEF(...)CallAt(AnyProcAddress);
APICall AnyName([...])
//as usual

NOTE: In case of CALLAT the name of the buffer variable is not important. The user can choose any name. See CMenus sample class as an example of this feature.

How a Buffer variable is used to make the API call

APICall has all the information to make the call using the name of the Buffer variable (or the one provided via USE keyword in its creation) and the DLL defined in its creation (via keyword API). For the parameters of the call scans the fields of the Buffer variable and pushes them to the stack obeying in the following rules:

If there is a need to force a field to be passed by a pointer to its memory then we use symbol & in its structure definition:

set AddTwoNumbers=BUFDEF(num1,num2:&atINTEGER)API('MATH.DLL');

The use of symbol & has no effect in fields that are passed always by pointer. Also it doesn't affect the use of Buffer variables in any other way.

NOTE: In case the CALLAT keyword is used instead of API then the name of the Buffer variable or anything defined via USE keyword is not used.

Importing functions with function pointer parameters (CALLBACKs)

In case of a function pointer parameter or in any case we need to pass a pointer to a function (callback), we use the command CallBack. This command returns the address of a stub routine (in native processor's code) which its only purpose is to redirect execution to a local CalcIt function.

CallBack(LocalFunc,BUFDEFID);

The above code fragment returns the address of a stub routine that when it is called will redirect the execution to the CalcIt local function LocalFunc. The interface of the callback routine is described by the Buffer definition BUFDEFID. Even the callback routines can have various interfaces the CalcIt local function, where the execution is redirected, has always the same interface:

function LocalFunc(buf p);

There is only one parameter of Buffer type (p in the above example). Through this Buffer parameter the parameters of the real callback can be accessed.  Buffer definition defined in CallBack command is assumed  (BUFDEFID in the example).

To describe an API call, having function pointer parameters, via a Buffer variable,  atCALLBACK type can be used:

set EnumFontFamiliesExA = BUFDEF(hdc:atINTEGER, lpLogfont:atBUFFER(LOGFONT), lpEnumFontFamExProc:atCALLBACK, lParam, dwFlags:atDWORD)API('GDI32.DLL');

This type (atCALLBACK) is provided for readability reasons as atINTEGER or atPOINTER or atDWORD can be used as well (32bit integers).

The above Buffer variable describes an actual Windows API routine to enumerate all fonts in the system:

const ENUMLOGFONTEX=BUFDEF(
elfLogFont:atBUFFER(LOGFONT),
elfFullName:atCHAR[LF_FULLFACESIZE],
elfStyle:atCHAR[LF_FACESIZE],
elfScript:atCHAR[LF_FACESIZE]
);

const EnumFontFamExProcInterf=BUFDEF(
lpelfe:atPOINTER,
lpntme:atPOINTER,
FontType:atDWORD,
lParam:atDWORD
);

const FontBf=BUFDEF(sz:atINTEGER,s:atCHAR[100](1000));

function EnumFontFamExProc(buf bf);
 set dd =BUFFER(FontBf)ADDR(bf.lParam);
 set fnt=BUFFER(ENUMLOGFONTEX)ADDR(bf.lpelfe);
 s:=str(fnt.elfFullName,'(',fnt.elfScript,')');
 dd.s(dd.sz):=s;
 dd.sz++;
 1;
end;


function wGetFonts(arr& Fnts);public;
 dc:=APICall GetDC([0]);
 set bf=BUFFER(FontBf);
//intermediate font info buffer
 EnumFontFamiliesExA.lpEnumFontFamExProc:=CallBack(EnumFontFamExProc,EnumFontFamExProcInterf);
 EnumFontFamiliesExA.lpLogfont.lfCharset:=DEFAULT_CHARSET;
 EnumFontFamiliesExA.lParam:=&bf;
//pass intermediate buffer bf where
                                  //font info will be placed by CallBack routine

 APICall EnumFontFamiliesExA([dc]);
//call Window API Font routine

//copy info from intermediate buffer to array Fnts
 APICall ReleaseDC([0,dc]);
 set Fnts(bf.sz);
 for(i,1,bf.sz);
  Fnts(i):=bf.s(i-1);
 end;
end;

The above code fragment is taken from the CWinAPI sample Class. wGetFonts, when it is called, returns in its array IN/OUT parameter Fnts all system fonts. Using command CallBack we pass in the appropriate parameter of EnumFontFamiliesExA the address of a stub routine which when called by EnumFontFamiliesExA redirects the execution to the EnumFontFamExProc local CalcIt function. There by using buffer variable bf we decode the actual callback function parameters and proceed normally as the documentation of the specific Window API  routine instructs.

NOTE: The above discussion assumes callbacks that follow Windows API calling conventions and return an integer value or pointer (32 bit value). In case of C/C++ calling conventions or any other supported returned type there are two more optional parameters. See Callback command description for more details.

Example of Windows API Import

The code below imports some functions (Arc, GetDC, ReleaseDC) of the Windows API and draws concentric circles in the screen:

Clear;

set GetDC=BUFDEF(h:atINTEGER)API('USER32.DLL');
set ReleaseDC=BUFDEF(h,dc:atINTEGER)API('USER32.DLL');
set Arc=BUFDEF(
 hdc,
 nLeftRect,
 nTopRect,
 nRightRect,
 nBottomRect,
 nXStartArc,
 nYStartArc,
 nXEndArc,
 nYEndArc:atINTEGER
)API('GDI32.DLL');


function Circle(dc,x,y,Radious);
 APICall Arc([dc,x-Radious,y-Radious,x+Radious,y+Radious,x,y-Radious,x,y-Radious]);
end;

function Concentric;
 dc:=APICall GetDC([0]);
 for(i,10,600,20);
  Circle(dc,500,300,i);
 end;
 APICall ReleaseDC([0,dc]);
end;

Concentric;

NOTE: In the sample Class CWinAPI you can find a huge number of Window API imports.