Clipper è un linguaggio compilato, sintatticamente compatibile in
larga misura con l'interprete del dBase III, orientato al database management.
Sin dalle prime versioni, Clipper ha implementato gli strumenti necessari
all'interfacciamento con il C per la realizzazione di funzioni non presenti
nelle sue librerie standard (soprattutto per la gestione della macchina
a basso livello).
A tal fine è raccomandato l'utilizzo del Microsoft C (del resto
le librerie Clipper sono scritte in Microsoft C); in realtà tutti
i compilatori C in grado di generare, per il large memory model, moduli
oggetto compatibili con l'architettura DOSSEG definita da Microsoft possono
essere validamente impiegati in molti casi[1].
Il Microsoft C è indispensabile, per motivi di compatibilità,
solo nel caso in cui si vogliano realizzare funzioni implementanti matematica
in virgola mobile o routine grafiche[2].
Le funzioni scritte in C devono quindi essere compilate per il modello
di memoria large[3] (vedere il capitolo dedicato)
e, se si utilizza il compilatore Microsoft, deve essere richiesta l'opzione
"Floating Point Alternate Library" (/FP); è inoltre
molto comodo includere nel sorgente il file EXTEND.H, fornito
con il pacchetto Clipper, che definisce alcune macro e costanti manifeste
e contiene i prototipi di tutte le funzioni, facenti parte nella libreria
Clipper, che consentono lo scambio di parametri e valori restituiti tra
le funzioni Clipper e quelle C. Il modulo oggetto risultante dalla compilazione
può essere collegato all'object file prodotto da Clipper, oppure
può essere inserito in una libreria.
Passaggio di parametri e restituzione di valori
Nello scrivere funzioni C richiamabili da programmi Clipper va tenuto sempre
presente che non ha senso, per esigenze di compatibilità tra i due
linguaggi, parlare di parametri formali. Le
funzioni C possono accedere a tutti i parametri attuali della chiamata
Clipper invocando le funzioni _par...() (facenti parte della libreria
Clipper e dichiarate in EXTEND.H) e devono pertanto essere dichiarate
prive di parametri formali (void). Vi sono numerose funzioni _par...(),
ciascuna dedicata ad un particolare tipo di parametro.
Anche l'eventuale restituzione di un valore alla routine Clipper deve avvenire
mediante le funzioni _ret...(), facenti parte della libreria Clipper
e tra loro diversificate in base al tipo del valore da restituire. Le funzioni
_ret...() non restituiscono il controllo alla routine Clipper,
ma si limitano a predisporre la restituzione del valore; la funzione C
cede il controllo solo al termine del proprio codice o eseguendo un'istruzione
return). Ne segue che le funzioni C devono essere dichiarate void;
inoltre le specifiche Clipper impongono che esse siano dichiarate anche
pascal. Il file EXTEND.H
definisce la macro CLIPPER, che può essere usata per dichiarare
le funzioni C ed equivale proprio a void pascal.
Il prototipo di una funzione C richiamabile da Clipper è perciò
analogo a[4]:
Nell'ipotesi che funzPerClipper() accetti 4 parametri, la chiamata
in Clipper è:
RETVAL = FUNZPERCLIPPER(PAR_1,PAR_2,PAR_3,PAR_4)
Si tratta ora di analizzare brevemente le funzioni _par...() e
_ret...(), per scoprire come funzPerClipper() può
accedere a PAR_1, PAR_2, PAR_3 e PAR_4
e restituire RETVAL.
La funzione
int _parinfo(int order);
restituisce il tipo del parametro che occupa la posizione order
nella lista dei parametri attuali; _parinfo(0) restituisce il
numero di parametri passati alla funzione. Il tipo è identificato
dalle seguenti costanti manifeste, definite in EXTEND.H:
#define UNDEF 0
#define CHARACTER 1
#define NUMERIC 2
#define LOGICAL 4
#define DATE 8
#define MPTR 32 /* sommato al tipo effettivo se passato per reference */
#define MEMO 65
#define ARRAY 512
La costante MPTR ha un significato particolare: essa è
sommata al tipo del parametro per indicare che esso è stato passato
come reference (preceduto, secondo la sintassi Clipper, da una @):
perciò, se _parinfo(1) restituisce 33, significa
che il primo parametro attuale è una stringa passata per reference.
La funzione
int _parinfa(int order,int index);
restituisce il tipo dell'elemento in posizione index nell'array
che a sua volta è parametro attuale di posizione order.
Così, se _parinfa(2,5) restituisce 1, significa
che il quinto elemento dell'array ricevuto come secondo parametro attuale
è una stringa[5]. La chiamata
_parinfa(order,0);
restituisce il numero di elementi dell'array che occupa la posizione order
tra i parametri attuali. Il secondo parametro (index) può
essere omesso: in tal caso _parinfa() equivale a _parinfo()[6].
Vediamo ora una rapida rassegna delle altre principali funzioni _par...().
Per i dettagli sintattici si rimanda alla documentazione del linguaggio
Clipper; va detto, tuttavia, che nell'uso di tali funzioni il secondo parametro,
index, può essere omesso: in particolare, esso deve essere
utilizzato solo quando il parametro attuale la cui posizione è espressa
dal primo parametro order sia un array. In tal caso, index
individua uno specifico elemento all'interno dell'array medesimo.
La funzione
int _parni(int order,int index);
"recupera" il parametro attuale di posizione order (se
esso è un array è possibile specificarne un singolo elemento
con index; in caso contrario index può essere
omesso) e lo restituisce sotto forma di intero: ad esempio, la chiamata
#include <extend.h>
....
int parm_1;
....
parm_1 = _parni(1);
....
consente di accedere al primo parametro, un integer, passato dalla routine
Clipper alla funzione C. Del tutto analoghe alla _parni() sono
le funzioni
long _parnl(int order,int index);
double _parnd(int order,int index);
int _parl(int order,int index);
char *_pards(int order,int index);
char *_parc(int order,int index);
Tutte, infatti, restituiscono il parametro attuale di posizione order
(con la possibilità, se esso è un array, di individuarne
un elemento mediante index). La _parnl() restituisce
un long e la _parnd() un double, mentre la _parl()
restituisce un intero che rappresenta un campo logico Clipper (0
equivale a .F. e 1 a .T.).
La _pards() restituisce una stringa derivata da un campo Clipper
di tipo date; la data è espressa nella forma "AAAAMMGG".
E' opportuno copiare la stringa restituita in un buffer appositamente allocato,
in quanto il buffer gestito dalla funzione _pards() è statico
e viene sovrascritto ad ogni chiamata alla stessa. Ad esempio, nel codice
la printf() visualizza due volte la data passata come secondo
parametro, perché la seconda chiamata a _pards() sovrascrive
il buffer statico che essa utilizza internamente. Per ottenere un funzionamento
corretto è sufficiente apportare la modifica seguente:
La funzione _parc() restituisce un puntatore a carattere. Va però
ricordato che la gestione delle stringhe in Clipper è differente
da quella implementata in C: in particolare, le stringhe possono contenere
anche caratteri nulli (lo zero binario). Per conoscere la lunghezza effettiva
di una stringa Clipper può essere utilizzata la funzione
int _parclen(int order,int index);
che non include nel computo il NULL che chiude la stringa stessa,
mentre la
int _parcsiz(int order,int index);
restituisce il numero di byte effettivamente allocati per contenere la
stringa (o, più in generale, l'array di caratteri), incluso l'eventuale
NULL che la chiude. Se il parametro è una costante stringa,
_parcsiz() restituisce 0.
Passiamo ora ad un rapido esame delle funzioni _ret...(), che
consentono alla funzione C di restituire un valore alla routine Clipper,
con la precisazione che, essendo possibile restituire un unico valore,
la funzione C può chiamare una sola volta una funzione _ret...()
prima di terminare.
E' il caso di citare per prima una funzione leggermente particolare: si
tratta della
void _ret(void);
che non restituisce a Clipper alcun valore (esattamente come se la funzione
C terminasse con una semplice return), ma gestisce lo stack in
modo tale che la routine Clipper possa chiamare la funzione C con un'istruzione
DO (come se fosse, cioè, una procedure e non una
function).
Vi è poi una _ret...() per ogni tipo di dato, analogamente
a quanto visto per le funzioni _par...():
La _retni() restituisce alla routine Clipper, sotto forma di campo
numerico, l'intero che le è passato come parametro. Del tutto analoghe
sono la _retnl() e la _retnd(), che devono essere utilizzate
per restituire, rispettivamente, un long e un double.
La _retl() restituisce a Clipper, sotto forma di campo logico,
l'intero che riceve come parametro. Questo deve rappresentare un valore
booleano, cioè deve essere 1 o 0, convertiti rispettivamente
in .T. e .F..
La _retds() restituisce sotto forma di campo date la stringa
passatale come parametro, che deve essere nella forma "AAAAMMGG".
La _retc() restituisce a Clipper il puntatore a carattere (stringa)
che le è passato come parametro. Alla _retc() si affianca
la
void _retclen(char *buffer,int len);
che restituisce al Clipper, oltre all'indirizzo della stringa, anche la
lunghezza della medesima. La _retclen() si rivela utile nei casi
in cui il puntatore a carattere indirizza un buffer contenente anche byte
nulli (zeri binari).
Reference e puntatori
Con Clipper Summer '87 non esiste modo per passare ad una funzione C l'indirizzo
di una variabile Clipper (cioè il puntatore ad essa): ciò
avviene, ma implicitamente, solo nel caso delle stringhe. La libreria Clipper
5 include invece alcune funzioni, le _stor...(), che consentono
al codice C di accedere alle variabili ricevute per reference, cioè
passate come parametri attuali anteponendo al nome il carattere @,
in modo analogo a quello realizzabile (in C puro) mediante il passaggio
di puntatori. La documentazione Clipper sottolinea che passare per reference
una variabile ad una funzione C non significa comunque passarne l'indirizzo:
in realtà la gestione del passaggio di parametri è, in Clipper,
piuttosto complessa; tuttavia l'utilizzo dei reference comporta l'inserimento
nello stack di informazioni relative all'indirizzo della variabile, ed
è proprio grazie a tali informazioni che le nuove funzioni cui si
è accennato consentono di "simulare" l'impiego di veri
e propri puntatori.
La funzione
int _storni(int n,int order,int index);
memorizza l'intero n nella variabile Clipper passata via reference
alla funzione C come parametro di posto order; il terzo parametro
è facoltativo e deve essere utilizzato solo nel caso in cui il parametro
ricevuto in posizione order sia un array: in tal caso index
individua un preciso elemento al suo interno. La funzione restituisce 0
in caso di errore, 1 altrimenti.
Analoghe alla _storni() sono le funzioni
int _stornd(double n,int order,int index);
int _stornl(long n,int order,int index);
int _storl(int logical,int order,int index);
int _stords(char *string,int order,int index);
int _storc(char *string,int order,int index);
La _stornd() è utilizzabile per memorizzare un valore come
double, mentre la _stornl() memorizza un long integer.
La _storl() accetta come primo parametro un intero e lo memorizza
come valore logico (0 equivale a .F.; un valore diverso
da 0 equivale a .T.); la _stords() memorizza
come data la stringa string, che deve avere formato "AAAAMMGG".
La _storc() memorizza una stringa; in alternativa può essere
utilizzata la
int _storclen(char *buffer,int length,int order,int index);
che consente di memorizzare un array di caratteri indicandone la
lunghezza[7].
Allocazione della memoria
Le funzioni C destinate ad interfacciarsi con Clipper non possono gestire
l'allocazione dinamica della memoria mediante malloc() e le altre
funzioni della libreriaC allo scopo predisposte:
per esigenze di compatibilità è necessario utilizzare due
funzioni che la libreria Clipper Summer '87 rende disponibili, ancora una
volta, tramite i prototipi dichiarati in EXTEND.H. In particolare,
la funzione
unsigned char *_exmgrab(unsigned int size);
alloca in modo compatibile con Clipper un buffer ampio size byte
e ne restituisce l'indirizzo sotto forma di puntatore a unsigned char[8]. In caso di errore viene restituito NULL.
L'analogia con malloc() è evidente. La memoria allocata
da _exmgrab() può essere gestita con le comuni tecniche
C (puntatori, indirezioni, etc.); tuttavia essa deve essere disallocata
con l'apposita funzione Clipper:
void _exmback(unsigned char *pointer,unsigned int size);
che svolge un ruolo analogo a quello della funzione C free();
a differenza di questa, però, _exmback() richiede che,
oltre all'indirizzo dell'area di memoria da disallocare, le sia passato
anche il numero di byte da liberare. Se si intende disallocare l'intera
area di RAM, il parametro size passato a _exmback() deve
essere identico a quello omologo passato a _exmgrab(). Vediamo
un semplice esempio:
L'indirizzo di un buffer allocato da _exmgrab() può essere
restituito a Clipper tramite la _retclen(); con riferimento all'esempio
precedente la chimata potrebbe essere
_retclen(buffer,size);
E' palese che il buffer non deve essere disallocato né prima né
dopo la chiamata a _retclen().
La libreria del più recente Clipper 5 comprende invece 3 funzioni
per l'allocazione dinamica della RAM, due delle quali sostituiscono quelle
appena descritte. La
void *_xalloc(unsigned int size);
rimpiazza la _exmgrab(). Anche la _xalloc() restituisce
NULL in caso di errore. Alla _xalloc() si affianca una
funzione di nuova concezione, la
void *_xgrab(unsigned int size);
alloca anch'essa size byte nello heap di Clipper ma, a differenza
della _xalloc() genera un runtime error in caso di errore.
La _exmback() è stata sostituita dalla
void _xfree(void *mem);
Come si vede, la _xfree() accetta un unico parametro, rappresentante
l'indirizzo dell'area di memoria da liberare: non è più possibile,
quindi, richiedere la disallocazione di una parte soltanto della memoria
in precedenza allocata.
Alcuni esempi
Presentiamo di seguito alcuni esempi di funzioni C richiamabili da programmi
Clipper. La prima permette di utilizzare alcuni servizi DOS da procedure
Clipper.
/********************
BARNINGA_Z! - 1993
CL_BDOS.C - cl_bdos()
void pascal cl_bdos(void);
Sintassi per Clipper:
int cl_bdos(int dosfn,int dosdx,int dosal);
int dosfn numerico intero rappresentante il numero di servizio dell'int 21h
int dosdx numerico intero rappresentante il registro DX
int dosal numerico intero rappresentante il registro AL
Restituisce: il valore restituito dall'int 21h nel regsitro AX
COMPILABILE CON MICROSOFT C 5.1
CL /c /AL /Oalt /FPa /Gs /Zl cl_bdos.c
********************/
#include <extend.h>
#include <dos.h>
#define PNUM 3
#define ERROR -1
CLIPPER cl_bdos(void)
{
register i;
int fn, dx, al;
if(_parinfo(0) != PNUM) { // la proc. Clipper chiamante passa 3 param.
_retni(ERROR);
return;
}
for(i = 1; i <= PNUM; i++)
if(_parinfo(i) != NUMERIC) {
_retni(ERROR);
return;
}
_retni(bdos(_parni(1),_parni(2),_parni(3)));
return;
}
Ed ecco un esempio di chiamata alla cl_bdos() in una routine Clipper:
DOS_FN = 12 && servizio 0Ch int 21h (vuota buffer tastiera e invoca altro servizio)
REG_AL = 7 && servizio da invocare in servizio 0Ch (7 = attende tasto)
RET_VAL = CL_BDOS(DOS_FN,0,REG_AL) // chiama interrupt 21h
IF RET_VAL = -1
@ 10,12 SAY "Errore!"
ELSE
RET_VAL = RET_VAL % 256 // calcola AL
.... // utilizza il valore restituito (tasto premuto)
ENDIF
Il frammento di codice Clipper presentato utilizza la cl_bdos()
per invocare l'int 21h, servizio 0Ch, con AL = 7: l'operazione
che il servizio esegue consiste nel vuotare il buffer di tastiera e interrompere
l'esecuzione del programma in attesa della pressione di un tasto. La cl_bdos()
restituisce il valore di AX a sua volta restituito dall'int 21h:
da questo viene poi "estratto" il valore di AL, cioè
il codice ASCII del tasto premuto[9]. La formule
per ricavare il valore di un registro a 16 bit a partire dai due sottoregistri
a 8 bit che lo compongono è molto semplice:
RX = (RH * 256) + RL
dove RX indica un generico registro a 16 bit, mentre RH
e RL rappresentano, rispettivamente, il sottoregistro "alto"
(gli 8 bit più significativi) e quello "basso" (gli 8
bit meno significativi). Inoltre sono valide le seguenti:
RH = RX / 256
RL = RX % 256
Si può cioè affermare che gli 8 bit più significativi
sono ottenibili dividendo il valore a 16 bit per 256, senza considerare
resto o decimali, mentre gli 8 bit meno significativi sono il resto della
precedente divisione.
Ed ora il secondo esempio. E' noto che in ogni programma C main()
può essere dichiarata con alcuni parametri
formali: il secondo di questi, solitamente chiamato argv,
è un array di puntatori a stringa (o, meglio, a carattere: char
**argv), la prima delle quali (argv[0]) rappresenta il nome
del programma eseguibile, completo di pathname. In Clipper è possibile
accedere ai parametri della command line[10],
ma non si ha modo di conoscere, in modo quasi "automatico", come
nei programmi C, nome e pathname del programma stesso. L'ostacolo può
essere aggirato con una funzione C, il cui modulo oggetto deve essere collegato
all'object file prodotto dal compilatore Clipper.
/********************
BARNINGA_Z! - 1991
CL_EXENM.C - cl_exename()
void pascal cl_exename(void);
char *cl_exename();
Restituisce: il puntatore ad una stringa che rappresenta il nome del
programma eseguibile completo di pathname
COMPILABILE CON BORLAND C++ 3.1
tcc -O -d -c -ml cl_exenm.c
********************/
#include <extend.h>
#include <dir.h>
#include <dos.h>
CLIPPER cl_exename(void)
{
register i;
unsigned PSPseg;
char *ENVptr;
static char exeName[MAXPATH];
_AH = 0x62;
geninterrupt(0x21);
PSPseg = _BX;
ENVptr = MK_FP(*(unsigned far *)MK_FP(PSPseg,0x2C),0);
for(;;) {
if(!*ENVptr++)
if(!*ENVptr)
break;
}
for(; *ENVptr != 1;)
ENVptr++;
for(ENVptr += 2; *ENVptr; )
exeName[i++] = *ENVptr++;
exeName[i] = 0;
_retc(exeName);
}
La cl_exename() utilizza il servizio 62h dell'int 21h per conoscere
l'indirizzo di segmento del Program Segment Prefix
del programma e lo utilizza per costruire un puntatore all'environment,
cioè alla prima delle variabili d'ambiente. Infatti, la word ad
offset 2Ch nel PSP rappresenta l'indirizzo di segmento dell'area allocata
dal DOS all'environment. L'espressione
*(unsigned far *)MK_FP(PSPseg,0x2C)
restituisce detta word, pertanto l'espressione
MK_FP(*(unsigned far *)MK_FP(PSPseg,0x2C),0)
restituisce il puntatore (far) alla prima stringa dell'environment.
Le stringhe rappresentanti variabili d'ambiente sono memorizzate l'una
di seguito all'altra; ogni stringa è terminata da un byte nullo,
che in questo caso funge anche da "separatore". L'ultima stringa
nell'environment è conclusa da due byte nulli: la sequenza 00h
00h, che indica anche la fine dell'environment, è ricercata
dal primo ciclo for. Il secondo ciclo cerca il byte 01h, che segnala
la presenza della stringa contenente nome e path dell'eseguibile: il byte
01h è seguito da un byte nullo, dopo il quale inizia la stringa;
la clausola di inizializzazione
ENVptr += 2;
del terzo ciclo for "scavalca" la sequenza 01h 00h;
l'istruzione che costituisce il corpo del ciclo stesso può così
copiare la stringa, un byte ad ogni iterazione, nel buffer statico exeName.
La scelta dell'allocazione statica evita il ricorso a _exmgrab();
del resto non sarebbe possibile dichiarare il buffer come semplice variabile
automatica in quanto l'area di memoria da esso occupata verrebbe rilasciata
in uscita dalla funzione.
La stringa è esplicitamente terminata da un byte nullo e il suo
indirizzo è restituito a Clipper mediante _retc().
La chiamata a cl_exename() in Clipper può essere effettuata
come segue:
EXEPATH = ""
....
EXEPATH = CL_EXENAME()
@ 10,12 say "Questo programma è " + EXEPATH
....
Qualora il programma Clipper deallochi il proprio environment (eventualmente
utilizzando una funzione scritta in C), è indispensabile che la
chiamata a cl_exename() avvenga prima di detta operazione.
Va ancora sottolineato che il sorgente di cl_exename() può
essere compilato con il compilatore Borland: d'altra parte non referenzia
alcuna funzione di libreria C.
Vediamo un ultimo esempio: una funzione in grado di suddividere un numero
in virgola mobile in parte intera e parte frazionaria, basata sulla funzione
di libreria C modf().
/********************
BARNINGA_Z! - 1994
CL_MODF.C - cl_modf()
void pascal cl_modf(void);
Sintassi per Clipper:
double cl_modf(double n,double @ipart);
double n numerico in virgola mobile che si vuole suddividere in parte
intera e parte frazionaria
double @ipart reference a numerico in virgola mobile destinato a contenere
la parte intera
Restituisce: la parte frazionaria di n
COMPILABILE CON MICROSOFT C 5.1
CL /c /AL /Oalt /FPa /Gs /Zl cl_modf.c
********************/
#include <extend.h>
#include <math.h>
#define PNUM 2
#define ERROR 0.0
CLIPPER cl_modf(void)
{
double ip, fp;
if(PCOUNT != PNUM) { // la proc. Clipper chiamante passa 2 param.
_retnd(ERROR);
return;
}
if(! ISNUM(1)) {
_retnd(ERROR);
return;
}
if(!(ISNUM(2) && ISBYREF(2))) {
_retnd(ERROR);
return;
}
fp = modf(_parnd(1),&ip);
if(!_storni(ip,2)) {
_retnd(ERROR);
return;
}
_retnd(fp);
return;
}
Segue esempio di chiamata alla cl_modf() in una routine Clipper:
DOUBLE_NUM = 12.5647
INT_PART = 0.0
FRAC_PART = CL_MODF(DOUBLE_NUM,@INT_PART)
@ 10,12 SAY "PARTE INTERA: "+STR(INT_PART)+" PARTE FRAZIONARIA: "+STR(FRAC_PART)
Il programma Clipper visualizza il numero 12 come parte intera
e il numero 0.5647 come parte frazionaria. La restituzione di
due valori (parte intera e parte frazionaria di DOUBLE_NUM) alla
routine chiamante è resa possibile dal passaggio per reference della
variabile INT_PART alla CL_MODF(): questa, mediante la
_stornd() è in grado di memorizzare all'indirizzo della
INT_PART, cioè nella INT_PART stessa, il valore
che, a sua volta, la modf() ha scritto all'indirizzo di ip,
cioè nella ip medesima.
E' evidente che l'utilizzo delle macro PCOUNT, ISNUM()
e ISBYREF(), nonché della funzione _storni(),
rende la CL_MODF() utilizzabile esclusivamente da programmi compilati
da Clipper 5.
OK, andiamo avanti a leggere il libro...