Parallelamente al progetto che sto intraprendendo attualmente, ho iniziato a seguire un
"percorso alternativo": sto cercando di vedere se e come realizzare un software
GIS su piattaforme PocketPC.
Nell'analisi delle possibilità di sviluppo del
software, ho verificato che non esiste la possibilità di gestire via palmare i
file .dbf tramite codice managed, sia tramite il Compact
Framework 1/2, che tramite estensioni OpenNET o componenti esterni.
Siccome
xò i file dbf sono alla base della gestione degli attributi nei file shapefile
(PDF delle specifiche del formato di file Shapefile), ed è
essenziale che qualsiasi cosa si chiami GIS gestisca bene questo formato di file
(che sono nei GIS l'equivalente dei documenti word nel programmi di word
processing, o dei TIFF/JPG nei software di manipolazione immagini), sono stato
costretto a ripiegare su componenti unmanaged.
Essendo però abbastanza a
digiuno di C++ (in realtà sono un ignorante completo...) e volendo mantenere una
certa indipendenza in questo progetto dal mio gruppo di colleghi (che al
contrario ha discrete skills in Visual C++ 6), ho pensato bene (mica tanto bene
in realtà...) di provare ad effettuare un porting su dispositivi PocketPC di una
libreria, ShapeLib, scritta in C e direttamente utilizzabile in .NET.
Sinceramente all'inizio pensavo che semplicemente ricompilando il codice con
il compilatore Embedded Visual C++ avrei potuto utilizzare la
libreria anche su dispositivi PocketPC senza troppi patemi d'animo... mai
previsione fu + azzardata! :( La realtà si sta rivelando infatti assai più
complessa...
Per prima cosa, i dispositivi Pocket PC pare supportino solo i
caratteri di tipo UNICODE (sicuramente il Compact Framework 1 non permette di
chiamare via P/Invoke codice ANSI, visto che l'enumerazione CharSet non permette che di scegliere di fatto i
caratteri UNICODE), mentre la libreria ShapeLib usa caratteri ANSI: questo
letteralmente rende il tutto un casino!
Ora, io non ho titolo alcuno per
spiegare come si porta del codice pensato per gestire i caratteri ANSI a
lavorare correttamente con caratteri UNICODE, quello che posso dire è che,
grazie all'aiuto dei miei colleghi (alla faccia dell'indipendenza del lavoro...)
e a colpi di conversioni char*/wchar_t, strcmp/wcstcmp e 'C'/L'C', sono riuscito
a fare il refactoring del codice C (che poi alla fine è diventato codice C++...)
e a compilare correttamente i metodi statici che mi servono (essenzialmente la
lettura e scrittura dei DBF, come detto).
Una prima cosa sorprendente che ho riscontrato è questa; prendendo come
esempio un semplice metodo:
DBFHandle __declspec(dllexport) __stdcall DBFOpen( const wchar_t * pszDBFFile, const wchar_t * pszAccess );
Ebbene, la corrispondente dichiarazione DllImport:
[DllImport("shapelib.dll", CharSet=CharSet.Unicode)]
public static extern IntPtr DBFOpen (string szDBFFile, string szAccess);
non funziona se si usa come dispositivo di deploy l'emulatore PocketPC 2002
(con processore emulato x86) distribuito con il Visual Studio 2003, mentre
funziona correttamente sul mio dispositivo PocketPC con processore ARM.
Il
motivo è questo: ovviamente ogni dispositivo richiede che la libreria a cui si
fa riferimento sia compilata per il proprio processore: il compilatore
eVC++ compila la libreria per dispositivi x86 modificando il nome del metodo da
DBFOpen a _DBFOpen@8 (mi è stato detto che è una
caratteristica del C++, e quindi la cosa è perfettamente "legale"), tale che la
seguente dichiarazione DllImport:
[DllImport("shapelib.dll", EntryPoint="_DBFOpen@8", CharSet=CharSet.Unicode)]
public static extern IntPtr DBFOpen (string szDBFFile, string szAccess);
funziona correttamente.
Lo stesso codice xò, compilato sempre in eVC++ ma
per dispositivi ARM non modifica il nome del metodo! Per dispositivi ARM occorre
quindi eliminare l'attributo EntryPoint="_DBFOpen@8" dalla dichiarazione DllImport.
Quello che in realtà mi ha sorpreso notevolmente è questo; prendiamo tre
metodi C++ correttamente implementati e funzionanti da codice unmanaged:
__declspec(dllexport) int __stdcall DBFReadIntegerAttribute( DBFHandle hDBF, int iShape, int iField,);
__declspec(dllexport) double __stdcall DBFReadDoubleAttribute( DBFHandle hDBF, int iShape, int iField,);
__declspec(dllexport) const wchar_t* __stdcall DBFReadStringAttribute( DBFHandle hDBF, int iShape, int iField,);
e le relative dichiarazioni DllImport:
[DllImport("shapelib.dll", CharSet=CharSet.Unicode)]
public static extern int DBFReadIntegerAttribute (IntPtr hDBF, int iShape, int iField);
[DllImport("shapelib.dll", CharSet=CharSet.Unicode)]
public static extern double DBFReadDoubleAttribute (IntPtr hDBF, int iShape, int iField);
[DllImport("shapelib.dll", CharSet=CharSet.Unicode)]
public static extern string DBFReadStringAttribute (IntPtr hDBF, int iShape, int iField);
Il primo metodo funziona correttamente, mentre i metodi successivi (quello
col double e la stringa come valore di ritorno) non solo non funzionano, ma a
runtime generano una NotSupportedException! Il fatto che codice unmanaged generi
a runtime una eccezione di tipo managed a me ha sorpreso assai, visto che ero
rimasto al fatto che per "grabbare" eccezioni non gestite fosse necessario
usare questa sintassi:
try
{
// Chiama codice gestito e non gestito
}
catch(Exception ex)
{
// Gestisci errori da codice managed
}
catch
{
// Gestisci errori derivanti da codice unmanaged
}
Immagino quindi che l'eccezione sia generata dall'attributo DllImport (anche
se lo stack delle chiamate non lo riporta) o dalla chiamata extern in qualche
punto del processo di esecuzione del programma PRIMA dell'ingresso nel codice
unmanaged.
Ho risolto la cosa modificando i metodi C++ in questo modo:
void __declspec(dllexport) __stdcall DBFReadIntegerAttribute( DBFHandle hDBF, int iShape, int iField, int *pReturnValue);
void __declspec(dllexport) __stdcall DBFReadDoubleAttribute( DBFHandle hDBF, int iShape, int iField, double *pReturnValue );
void __declspec(dllexport) __stdcall DBFReadStringAttribute( DBFHandle hDBF, int iShape, int iField, wchar_t *pReturnValue );
cioè passando come parametro ref il valore di ritorno della funzione, e
modificando ovviamente in modo opportuno le dichiarazioni DllImport; in questo
modo il codice funziona correttamente.
Chiunque possa segnalarmi risorse per approfondire la questione sarà
ringraziato ed elogiato privatamente e pubblicamente!
P.S: L'autore non si sente legalmente, materialmente e spiritualmente
responsabile delle castronerie eventualmente presenti nel testo precedentemente
riportato, dovute a fretta e/o ignoranza...
P.P.S: Nessun costrutto del linguaggio C++ è stato ucciso durante la stesura
del post...