I am writing a wrapper for the game programming library "Allegro" and its less stable 4.9 branch. Now, I have done good insofar, except for when it comes to wrapping a structure of function pointers. Basically, I can't change the original code, despite having access to it, because that would require me to fork it in some manner. I need to know how I can somehow pass a structure of delegates from managed to native without causing an AccessViolationException that has occurred so far.
Now, for the code. Here is the Allegro definition of the structure:
typedef struct ALLEGRO_FILE_INTERFACE
{
AL_METHOD(ALLEGRO_FILE*, fi_fopen, (const char *path, const char *mode));
AL_METHOD(void, fi_fclose, (ALLEGRO_FILE *handle));
AL_METHOD(size_t, fi_fread, (ALLEGRO_FILE *f, void *ptr, size_t size));
AL_METHOD(size_t, fi_fwrite, (ALLEGRO_FILE *f, const void *ptr, size_t size));
AL_METHOD(bool, fi_fflush, (ALLEGRO_FILE *f));
AL_METHOD(int64_t, fi_ftell, (ALLEGRO_FILE *f));
AL_METHOD(bool, fi_fseek, (ALLEGRO_FILE *f, int64_t offset, int whence));
AL_METHOD(bool, fi_feof, (ALLEGRO_FILE *f));
AL_METHOD(bool, fi_ferror, (ALLEGRO_FILE *f));
AL_METHOD(int, fi_fungetc, (ALLEGRO_FILE *f, int c));
AL_METHOD(off_t, fi_fsize, (ALLEGRO_FILE *f));
} ALLEGRO_FILE_INTERFACE;
My simple attempt at wrapping it:
public delegate IntPtr AllegroInternalOpenFileDelegate(string path, string mode);
public delegate void AllegroInternalCloseFileDelegate(IntPtr file);
public delegate int AllegroInternalReadFileDelegate(IntPtr file, IntPtr data, int size);
public delegate int AllegroInternalWriteFileDelegate(IntPtr file, IntPtr data, int size);
public delegate bool AllegroInternalFlushFileDelegate(IntPtr file);
public delegate long AllegroInternalTellFileDelegate(IntPtr file);
public delegate bool AllegroInternalSeekFileDelegate(IntPtr file, long offset, int where);
public delegate bool AllegroInternalIsEndOfFileDelegate(IntPtr file);
public delegate bool AllegroInternalIsErrorFileDelegate(IntPtr file);
public delegate int AllegroInternalUngetCharFileDelegate(IntPtr file, int c);
public delegate long AllegroInternalFileSizeDelegate(IntPtr file);
[StructLayout(LayoutKind.Sequential, Pack = 0)]
public struct AllegroInternalFileInterface
{
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalOpenFileDelegate fi_fopen;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalCloseFileDelegate fi_fclose;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalReadFileDelegate fi_fread;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalWriteFileDelegate fi_fwrite;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalFlushFileDelegate fi_fflush;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalTellFileDelegate fi_ftell;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalSeekFileDelegate fi_fseek;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalIsEndOfFileDelegate fi_feof;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalIsErrorFileDelegate fi_ferror;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalUngetCharFileDelegate fi_fungetc;
[MarshalAs(UnmanagedType.FunctionPtr)]
public AllegroInternalFileSizeDelegate fi_fsize;
}
I have a simple auxiliary wrapper that turns an ALLEGRO_FILE_INTERFACE into an ALLEGRO_FILE, like so:
#define ALLEGRO_NO_MAGIC_MAIN
#include <allegro5/allegro5.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
__declspec(dllexport) ALLEGRO_FILE * al_aux_create_file(ALLEGRO_FILE_INTERFACE * fi)
{
ALLEGRO_FILE * file;
assert(fi && "`fi' null");
file = (ALLEGRO_FILE *)malloc(sizeof(ALLEGRO_FILE));
if (!file)
return NULL;
file->vtable = (ALLEGRO_FILE_INTERFACE *)malloc(sizeof(ALLEGRO_FILE_INTERFACE));
if (!(file->vtable))
{
free(file);
return NULL;
}
memcpy(file->vtable, fi, sizeof(ALLEGRO_FILE_INTERFACE));
return file;
}
__declspec(dllexport) void al_aux_destroy_file(ALLEGRO_FILE * f)
{
assert(f && "`f' null");
assert(f->vtable && "`f->vtable' null");
free(f->vtable);
free(f);
}
Lastly, I have a class that accepts a Stream and provides the proper methods to interact with the stream. Just to make sure, here it is:
/// <summary>
/// A semi-opaque data type that allows one to load fonts, etc from a stream.
/// </summary>
public class AllegroFile : AllegroResource, IDisposable
{
AllegroInternalFileInterface fileInterface;
Stream fileStream;
/// <summary>
/// Gets the file interface.
/// </summary>
internal AllegroInternalFileInterface FileInterface
{
get { return fileInterface; }
}
/// <summary>
/// Constructs an Allegro file from the stream provided.
/// </summary>
/// <param name="stream">The stream to use.</param>
public AllegroFile(Stream stream)
{
fileStream = stream;
fileInterface = new AllegroInternalFileInterface();
fileInterface.fi_fopen = Open;
fileInterface.fi_fclose = Close;
fileInterface.fi_fread = Read;
fileInterface.fi_fwrite = Write;
fileInterface.fi_fflush = Flush;
fileInterface.fi_ftell = GetPosition;
fileInterface.fi_fseek = Seek;
fileInterface.fi_feof = GetIsEndOfFile;
fileInterface.fi_ferror = GetIsError;
fileInterface.fi_fungetc = UngetCharacter;
fileInterface.fi_fsize = GetLength;
Resource = AllegroFunctions.al_aux_create_file(ref fileInterface);
if (!IsValid)
throw new AllegroException("Unable to create file");
}
/// <summary>
/// Disposes of all resources.
/// </summary>
~AllegroFile()
{
Dispose();
}
/// <summary>
/// Disposes of all resources used.
/// </summary>
public void Dispose()
{
if (IsValid)
{
Resource = IntPtr.Zero; // Should call AllegroFunctions.al_aux_destroy_file
fileStream.Dispose();
}
}
IntPtr Open(string path, string mode)
{
return IntPtr.Zero;
}
void Close(IntPtr file)
{
fileStream.Close();
}
int Read(IntPtr file, IntPtr data, int size)
{
byte[] d = new byte[size];
int read = fileStream.Read(d, 0, size);
Marshal.Copy(d, 0, data, size);
return read;
}
int Write(IntPtr file, IntPtr data, int size)
{
byte[] d = new byte[size];
Marshal.Copy(data, d, 0, size);
fileStream.Write(d, 0, size);
return size;
}
bool Flush(IntPtr file)
{
fileStream.Flush();
return true;
}
long GetPosition(IntPtr file)
{
return fileStream.Position;
}
bool Seek(IntPtr file, long offset, int whence)
{
SeekOrigin origin = SeekOrigin.Begin;
if (whence == 1)
origin = SeekOrigin.Current;
else if (whence == 2)
origin = SeekOrigin.End;
fileStream.Seek(offset, origin);
return true;
}
bool GetIsEndOfFile(IntPtr file)
{
return fileStream.Position == fileStream.Length;
}
bool GetIsError(IntPtr file)
{
return false;
}
int UngetCharacter(IntPtr file, int character)
{
return -1;
}
long GetLength(IntPtr file)
{
return fileStream.Length;
}
}
Now, when I do something like this:
AllegroFile file = new AllegroFile(new FileStream("Test.bmp", FileMode.Create, FileAccess.ReadWrite));
bitmap.SaveToFile(file, ".bmp");
...I get an AccessViolationException. I think I understand why (the garbage collector can relocate structs and classes whenever), but I'd think that the method stub that is created by the framework would take this into consideration and route the calls to the valid classes. However, it seems obviously so that I'm wrong.
So basically, is there any way I can successfully wrap that structure?
(And I'm sorry for all the code! Hope it's not too much...)