суббота, 14 июля 2007 г.

Managed C++. Проба пера

Сегодня попробовал поработать с управляемым C++.

Ну и в качестве упражнения хотелось попробовать что-то интересное, а не ограничиваться переписыванием примеров из того же MSDN.

Поэтому мой выбор пал на одну небольшую библиотеку - minifmod. Это часть библиотеки FMOD - кроссплатформенной библиотеки для работы с различными аудиоформатами.

Библиотека minifmod поставляется с открытым исходным кодом, а так же имеет в поставке статическую C-библиотеку для использования их в проектах Visual C++.

Но .NET программист не сможет использовать эту библитеку, поэтому я решил это исправить.

Действительно, достаточно типичная ситуация. Мы имеем огромную массу библиотек, написанных на C и C++, для того чтобы использовать вызовы к этим библиотекам можно использовать достаточно удобный интерфейс как Platform Invoke. Однако в моем случае это было невозможно, потому как это не *.dll, а static link library.

Было принято решение создать проект на Managed C++, который является оберткой вокруг minifmod, а так же тестовый проект Windows Forms на C# который бы использовал мою библиотеку-обертку.

Изучив исходный код minifmod я обнаружил, что сама библиотека заточена на работу с музыкальными треками как с файлами:
FMUSIC_MODULE * FMUSIC_LoadSong(signed char *name, SAMPLELOADCALLBACK sampleloadcallback);

По правде говоря меня это несколько расстроило, потому как это накладывало целый ряд ограничений на ее использование.

К счастью, я обнаружил еще механизм функций обратного вызова, которые вселили в меня надежду что не все так плохо как кажется на самом деле:
typedef void (*FMUSIC_CALLBACK)(FMUSIC_MODULE *mod, unsigned char param);

void FSOUND_File_SetCallbacks(
unsigned int (*OpenCallback)(char *name),
void (*CloseCallback)(unsigned int handle),
int (*ReadCallback)(void *buffer, int size, unsigned int handle),
void (*SeekCallback)(unsigned int handle, int pos, signed char mode),
int (*TellCallback)(unsigned int handle));

Итак, я решил реализовать класс, который бы как минимум умел бы воспроизводить XM-треки из файла, и как максимум - работать с объектом типа System.IO.Stream.

Мне очень понравился процесс создания этой оболочки. Мне даже удалось в некоторой степени "обмануть" разработчиков, и передавать не строку с путем файла а указатель на объект типа System.IO.Stream.

Вот листинг .NET класса для воспроизведения XM-файлов:
namespace NModPlayer
{
public ref class ModPlayer : public IDisposable
{
private:
Stream^ _stream;
FMUSIC_MODULE* _mod;
GCHandle _gcHandle;

public:

ModPlayer(Stream^ stream)
{
_stream = stream;
FSOUND_File_SetCallbacks(memopen, memclose, memread, memseek, memtell);
}

void BeginPlay()
{
GCHandle _gcHandle = GCHandle::Alloc(_stream);
void* pStream = (void*)GCHandle::ToIntPtr(_gcHandle);

_mod = FMUSIC_LoadSong((char*)pStream, NULL);
FMUSIC_PlaySong(_mod);
}

void StopPlay()
{
if (_mod)
{
FMUSIC_FreeSong(_mod);
}

if (_gcHandle.IsAllocated)
{
_gcHandle.Free();
}
}

~ModPlayer()
{
StopPlay();
}
};
}

Обратите внимание на конструкцию вида:
GCHandle _gcHandle = GCHandle::Alloc(_stream);
void* pStream = (void*)GCHandle::ToIntPtr(_gcHandle);

_mod = FMUSIC_LoadSong((char*)pStream, NULL);

Сдесь мне пришел в голову один хак. Поскольку ответственность по реализации callback-функций я взял на себя, то я могу совершенно спокойно передавать что угодно.

Таким образом, я сделал фиксацию объекта Stream, получил на него неуправляемый указатель, привел его к типу который от меня хотели и передал в библиотечную функцию.

Реализация callback-функций выглядит следующим образом:
Stream^ handleToStream(unsigned int handle)
{
GCHandle gcHandle = GCHandle::FromIntPtr((IntPtr)(void*)handle);
Stream^ stream = (Stream^)gcHandle.Target;
return stream;
}

unsigned int memopen(char* instance)
{
return (unsigned int)((void*)instance);
}

void memclose(unsigned int)
{
// Do nothing because GC will free all allocated memory
}

int memread(void *buffer, int size, unsigned int handle)
{
Stream^ stream = handleToStream(handle);

if (stream->Position + size >= stream->Length)
{
size = (int)(stream->Length - stream->Position);
}

array^ managedBuffer = gcnew array(
(unsigned int)size);

stream->Read(managedBuffer, 0, size);

Marshal::Copy(managedBuffer, 0, (IntPtr)buffer, managedBuffer->Length);

return size;
}

void memseek(unsigned int handle, int pos, signed char mode)
{
Stream^ stream = handleToStream(handle);

if (mode == SEEK_SET)
{
stream->Position = pos;
}
else if (mode == SEEK_CUR)
{
stream->Position += pos;
}
else if (mode == SEEK_END)
{
stream->Position = stream->Length + pos;
}

if (stream->Position > stream->Length)
{
stream->Position = stream->Length;
}
}

int memtell(unsigned int handle)
{
Stream^ stream = handleToStream(handle);

return (int)stream->Position;
}

В фунции memopen вроде бы передают указатель на строку, но мы то знаем, что мы туда отдали ;)

В принципе реализация остальных функций не требует особых пояснений.

Так что теперь любой .NET программист сможет спокойно использовать эту библиотеку, потому как в его распоряжении окажется обычная .NET сборка с классом ModPlayer.

Вот например я этот класс использовал в Windows Form приложении:
namespace NMiniFMOD.Sample
{
public partial class FormMain : Form
{
ModPlayer _modPlayer;

public FormMain()
{
InitializeComponent();
}

private void FormMain_Shown(object sender, EventArgs e)
{
using (Stream stream = new MemoryStream(Resources.MusicTrack))
{
_modPlayer = new ModPlayer(stream);
_modPlayer.BeginPlay();
}
}

private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
{
_modPlayer.Dispose();
}
}
}

Если интересно, могу выложить полный исходный код.

Комментариев нет: