Написание виртуальной файловой системы на c++

Original author: Michael Walter
  • Translation
Еще одна моя запись из песочницы, если будет время то переведу остальные части

Это перевод первой части статьи про написание VFS (виртуальной файловой системы) на c++ которую я нашел достаточно давно. Надеюсь вам понравится. :)

Вступление


Когда я начал разработку своего 3D движка я понял что мне нужно что-нибудь вроде файловой системы. Не простой архив, а собственная виртуальная файловая система которая поддерживает сжатие, шифрование, имеет быстрое время доступа и тд.

И я решил выложить свои наработки чтобы вам не пришлось изобретать колесо. Эта статья будет разделена на 2 части. Первая это то что вы сейчас читаете и тут будет описанно строение VFS. Во второй части мы собственно и напишем саму VFS (она будет достаточно большой).

Так что такое VFS?


VFS это файловая система, похожая на аналогичные в Windows (fat32, ntfs и тд). Основное различие между VFS и реальной файловой системой в том что VFS использует реальную файловую систему внутри себя.

Функциональность


Назовем некоторые возможности VFS:
Быстрое время доступа
Несколько архивов вместо огромного количества маленьких файлов
Возможность дебагинга
Pluggable Encryption and Compression (PEC)
Защищенность (внутри файла vfs хранится MD5 ключ так что любые изменения архива тут же будут замечены)
Несколько root путей

Теперь так как мы перечислили список возможностей мы можем перейти к design фазе

Базовый дизайн


Давайте начнем: У нас будет главный интерфейс с 16 функциями. Я сначала вам покажу эти функции, потом мы обсудим для чего они и в следующей части мы их напишем.

#define VFS_VERSION 0x0100<br/>
#define VFS_PATH_SEPARATOR '\\'<br/>
<br/>
void VFS_Init();<br/>
void VFS_Shutdown();<br/>



Эти функции на самом деле ничего не делают кроме запуска / выгрузки некоторых структур которые нам понадобятся позже

Фильтры



typedef BOOL (* VFS_FilterProc )( LPCBYTE pIn, DWORD dwInCount, LPBYTE* ppOut, DWORD* pOutCount );<br/>
<br/>
struct VFS_Filter<br/>
{<br/>
string strName;<br/>

string strDescription;<br/>
VFS_FilterProc pfnEncodeProc;<br/>
VFS_FilterProc pfnDecodeProc;<br/>
};<br/>
<br/>
void VFS_RegisterFilter( VFS_Filter* pFilter );<br/>
void VFS_UnregisterFilter( VFS_Filter* pFilter );<br/>
void VFS_UnregisterFilter( DWORD dwIndex );<br/>

DWORD VFS_GetNumFilters();<br/>
const VFS_Filter* VFS_GetFilter( DWORD dwIndex );<br/>

Ну, это уже немного сложнее. VFS_Filter это своего рода подпрограмма, которая обрабатывает данные. Можно например написать VFS_CryptFilter который шифрует / расшифровывает данные. У тебя такое? Фильтр состоит из чего-то вроде предварительного процессора (процедура pfnEncodeProc) для данных записывающихся в архив и пост-процессора (процедура pfnDecodeProc) для считывания данных из архива. Эти фильтры реализуют Pluggable Encryption and Compression, упомянутые выше, поэтому вы можете назначить один или несколько фильтров для каждого vfs файла, которые вы используете. Если вы немного запутались, то посмотрите на рисунок 1, который представляет собой схему фильтров.



Вы видите, кодирующие / декодирующие процедуры манипулируют потоком данных в обоих направлениях: из архива в память и из памяти в архив.

Для лучшего понимания фильтров, давайте напишем простой фильтр (в любом случае имейте в виду, что мы не сможем проверить фильтр, потому что мы будем осуществлять VFS позже). Наш фильтр добавит 1 к каждому байту.

VFS_Filter ONEADD_Filter =<br/>
{<br/>
"ONEADD",<br/>
"This Filter adds 1 to each Byte of the Data. It doesn't really make sense, "<br/>
" but anyway, this is just a test, you know",<br/>

ONEADD_EncodeProc,<br/>
ONEADD_DecodeProc<br/>
};<br/>
<br/>
BOOL ONEADD_EncodeProc( LPCBYTE pIn, DWORD dwInCount, LPBYTE* ppOut, DWORD* pOutCount )<br/>
{<br/>
assert( ppOut );<br/>
assert( pOutCount );<br/>

<br/>
// Allocate the Memory.<br/>
*ppOut = new BYTE[ dwInCount ];<br/>
<br/>
// Perform a For-Loop through each Byte.<br/>
for( DWORD dwIndex = 0; dwIndex < dwInCount; dwIndex++ )<br/>
{<br/>

( *ppOut )[ dwIndex ] = ( BYTE )( pIn[ dwIndex ] + 1 );<br/>
}<br/>
<br/>
// Set the Output Count.<br/>
*pOutCount = dwInCount;<br/>
}<br/>
<br/>
BOOL ONEADD_DecodeProc( LPCBYTE pIn, DWORD dwInCount, LPBYTE* ppOut, DWORD* pOutCount )<br/>

{<br/>
assert( ppOut );<br/>
assert( pOutCount );<br/>
<br/>
// Allocate the Memory.<br/>
*ppOut = new BYTE[ dwInCount ];<br/>
<br/>
// Perform a For-Loop through each Byte.<br/>

for( DWORD dwIndex = 0; dwIndex < dwInCount; dwIndex++ )<br/>
{<br/>
( *ppOut )[ dwIndex ] = ( BYTE )( pIn[ dwIndex ] - 1 );<br/>
}<br/>
<br/>
// Set the Output Count.<br/>

*pOutCount = dwInCount;<br/>
}<br/>


Единственный смысл этого фильтра в том что можно сделать более сложным вскрытие vfs файла.

Функции root путей


Мы обсуждали функции root путей не так давно, помните? Если нет — не проблема. Я сказал, что мы хотим, функцию для использования нескольких root путей, то есть несколько путей поиска, как например, каталог установки программы, оптический диск и сетевой диск. Следующие функции будут использоваться для выполнения этого:

void VFS_AddRootPath( LPCTSTR pszRootPath );<br/>
void VFS_RemoveRootPath( LPCTSTR pszRootPath );<br/>
void VFS_RemoveRootPath( DWORD dwIndex );<br/>
DWORD VFS_GetNumRootPaths();<br/>
LPCTSTR VFS_GetRootPath( DWORD dwIndex );


Все достаточно легко, не так ли?

Немного простых, но нужных вещей



Следующие 4 функции достаточно просты:

void VFS_Flush();

Эта функция будет закрыть все открытые vfs файлы к которым не обращаются. Вы можете удивиться, почему vfs файл, к которому не обращаются не закрывается автоматически, но если мы так сделаем, то нам пришлось повторно анализировать vfs файл каждый раз, когда мы открываем или закрываем файл. Посмотрите на этот код, для дальнейшего объяснения:

// Reference Count is 1. -> Load + Parse!!!<br/>
DWORD dwHandle = VFS_File_Open( "Bla\\Bla.Txt" ); <br/>

<br/>
// Reference Count is 0. -> Close!!!<br/>
VFS_File_Close( dwHandle ); <br/>
<br/>
// Reference Count is 1. -> Load + Parse!!!<br/>
dwHandle = VFS_File_Open( "Bla\\Bla.Txt" );<br/>

<br/>
// Reference Count is 0. -> Close!!!<br/>
VFS_File_Close( dwHandle );<br/>


Вы видите, нам пришлось бы открывать архив файла два раза. Хорошее место для вызова VFS_Flush () В игре может быть, когда все данные уровня загрузились. Но вот последние 3 основные функции:

struct VFS_EntityInfo<br/>
{<br/>
BOOL bIsDir; // Is the Entity a Directory.<br/>

BOOL bArchived; // True if the Entity is located in an Archive.<br/>
string strDir; // like Models/Sarge/Textures<br/>
string strPath; // like Models/Sarge/Textures/Texture1.Jpg<br/>
string strName; // like Texture1.Jpg<br/>
DWORD dwSize; // The Number of Files and Subdirectories for a<br/>
Directory.<br/>

};<br/>
<br/>
BOOL VFS_Exists( LPCTSTR pszPath );<br/>
void VFS_GetEntityInfo( LPCTSTR pszPath, VFS_EntityInfo* pInfo );<br/>
DWORD VFS_GetVersion();<br/>


Первая функция в настоящее время проверяет существует ли обьект с путем pszPath. Вы видите, что это достаточно простая вещь, но в С (+ +) стандартная библиотека не содержит такую функцию (я знаю, они имеют такие функции, как stat(), но я хочу просто вещи вида exists() ). Вторая функция нечто вроде stat(), она возвращает информацию об обьекте

Последняя функцияне имеет ничего общего с информацией об обьекте, эта функция просто возвращает текущую версию VFS. Ничего особенного (Серьезно, он просто возвращает константу VFS_VERSION ;-)

Интерфейс работы с файлами



Мы рассмотрели простые вещи. Но не беспокойтесь, еще есть пару легких вещей впереди. В самом деле, все, описанное в этой части статьи является легким. К сожалению, если вы хотите, что-то сложнее, вам придется ждать следующей части этой статьи… ;-)

Ну, вот они, функции интерфейса работы с файлами:

#define VFS_INVALID_HANDLE ( ( DWORD ) -1 )<br/>
<br/>
// The VFS_File_Open/Create() Flags.<br/>
#define VFS_READ 0x01<br/>
#define VFS_WRITE 0x02<br/>

<br/>
// The VFS_File_Seek() Flags.<br/>
#define VFS_SET 0x00<br/>
#define VFS_CURRENT 0x01<br/>
#define VFS_END 0x02<br/>
<br/>
// Create / Open / Close a File.<br/>
DWORD VFS_File_Create( LPCTSTR pszFile, DWORD dwFlags );<br/>
DWORD VFS_File_Open( LPCTSTR pszFile, DWORD dwFlags );<br/>
void VFS_File_Close( DWORD dwHandle );<br/>

<br/>
// Read / Write from / to the File.<br/>
void VFS_File_Read( DWORD dwHandle, LPBYTE pBuffer, DWORD dwToRead, DWORD* pRead = NULL );<br/>
void VFS_File_Write( DWORD dwHandle, LPCBYTE pBuffer, DWORD dwToWrite, DWORD* pWritten = NULL );<br/>
<br/>
// Direct Data Access.<br/>
LPCBYTE VFS_File_GetData( DWORD dwHandle );<br/>
<br/>
// Positioning.<br/>
void VFS_File_Seek( DWORD dwHandle, LONG dwPos, DWORD dwOrigin = VFS_SET );<br/>

LONG VFS_File_Tell( DWORD dwHandle );<br/>
DWORD VFS_File_GetSize( DWORD dwHandle );<br/>
<br/>
// Information.<br/>
BOOL VFS_File_Exists( LPCTSTR pszFile );<br/>
void VFS_File_GetInfo( LPCTSTR pszFile, VFS_EntityInfo* pInfo );<br/>
void VFS_File_GetInfo( DWORD dwHandle, VFS_EntityInfo* pInfo );<br/>


Есть всего несколько вещей которые стоит отметить. Во-первых, параметр dwFlags у VFS_File_Create() и VFS_File_Open() может быть быть либо VFS_READ либо VFS_WRITE или сразу оба, а это означает доступ на чтение, запись или чтение / запись. Во-вторых, эти две функции возвращают handle, который используется почти всеми другими функциями, как своего рода указатель. Мы не будем использовать указатели, но будем использовать handle так как они обеспечивают еще один уровень абстракции. Еще я хотел бы упомянуть, тот факт, что наши функции будут загружать весь файл в память. Это необходимо в связи с особенностью фильтров (поскольку они нуждаются в памяти для обработки). Вы можете получить доступ к этой памяти непосредственно с помощью VFS_File_GetData(). Ну, а остальные в вещи вы должны знать, благодаря стандартной библиотеке ввода / вывода.

Интерфейс нашей библиотеки



Это может быть место которое вы ожидали, начиная с некоторых строк или лучше будет сказать страниц (и когда мы говорим об ожидании: тем чего я не ожидал, является тот факт, что это уже 7 страница или около этого. WOW!).

В любом случае, давайте продолжим:

// Create / Open / Close an Archive.<br/>
DWORD VFS_Archive_Create( LPCTSTR pszArchive, const VFS_FilterNameList& Filters, DWORD dwFlags );<br/>
DWORD VFS_Archive_CreateFromDirectory( LPCTSTR pszArchive, LPCTSTR pszSrcDir,<br/>

const VFS_FilterNameList& Filters, DWORD dwFlags );<br/>
DWORD VFS_Archive_Open( LPCTSTR pszArchive, DWORD dwFlags );<br/>
void VFS_Archive_Close( DWORD dwHandle );<br/>
<br/>
// Set the Filters used by this Archive.<br/>
void VFS_Archive_SetUsedFilters( DWORD dwHandle, const VFS_FilterNameList& Filters );<br/>
void VFS_Archive_GetUsedFilters( DWORD dwHandle, VFS_FilterNameList& Filters );<br/>
<br/>

// Add / Remove Files to / from the Archive.<br/>
void VFS_Archive_AddFile( DWORD dwHandle, LPCTSTR pszFile );<br/>
void VFS_Archive_RemoveFile( DWORD dwHandle, LPCTSTR pszFile );<br/>
<br/>
// Extract the Archive.<br/>
void VFS_Archive_Extract( DWORD dwHandle, LPCTSTR pszTarget );<br/>
<br/>
// Information.<br/>
void VFS_Archive_GetInfo( DWORD dwHandle, VFS_EntityInfo* pInfo );<br/>
void VFS_Archive_GetInfo( LPCTSTR pszArchive, VFS_EntityInfo* pInfo );<br/>



Очень простой интерфейс, не так ли? Просто обычные вещи для файла архива. И теперь вы, наконец, видите применение для функций фильтров которые мы видели до этого. Вы можете применять фильтры, с помощью VFS_Archive_Set / GetUsedFilters().

Интерфейс для работы с папками


Это последний интерфейс VFS. Он содержит 3 функции которые должны быть понятны без обьяснения как я думаю.

// Information.<br/>
BOOL VFS_Dir_Exists( LPCTSTR pszDir );<br/>
BOOL VFS_Dir_GetInfo( LPCTSTR pszDir, VFS_EntityInfo* pInfo );<br/>
<br/>

// Get the Contents of a Directory.<br/>
vector< VFS_EntityInfo > VFS_Dir_GetContents( LPCTSTR pszDir, BOOL bRecursive = FALSE );


Функции 1 и 2 достаточно простые (такие как будто они используются для файлов). Функция 3 действует как DOS команда. ;-)

Немного поболтаем


Вот и все. Мы закончили первую часть статьи. Я не верю (я тоже :) прим. переводчика). Но самое сложное еще впереди:

Нам нужно НАПИСАТЬ VFS!!!

Скачать article_vfs_header.h

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 9

    +1
    Мы что то подобное в институте делали на лабораторных работах.
      +1
      было бы здорово это в класс обвернуть
        0
        vector< VFS_EntityInfo >

        а если человек по каким-то причинам не может использовать STL?
          0
          Ну в данном случае это не рассматривается, но я думаю человеку который не использует stl и собирается писать vfs будет несложно заменить vector на что-то свое
          0
          писал свое когда то. у меня можно было прозрачно подключать т.н. namespaces (втыкались в дерево по указанному пути), которые могли быть архивами, реальными папками, экзотическими провайдерами данных (через TCP/IP гонял файлы с сервера), и т.п. сделано
            0
            оно было на C#, интерфейс был такой Stream Vfs.GetStream(string fullName). А дальше стандартные методы .NET для работы с потоковыми данными.

            сорри, случайно недописаную фразу запостил.
              0
              неплохо, весьма неплохо
                0
                угу, одна из самых интересных задач, пока я был программером. у меня там еще дерево namespaces было сделано плоским списком. к примеру, был namespace "/media/textures". из кода шел запрос Vfs.GetStream("/media/textures/bla/bla/bla/texture1.jpg". Vfs доставала провайдера, зарегестрированного на путь "/media/textures/" и передавало ему запрос на поиск «bla/bla/bla/texture1.jpg». т.е. там обхода дерева не было. довольно стройная система получилась =)

                если я не ошибаюсь, можно посмотреть на axiomengine.sourceforge.net в каком-то бранче.
            0
            Что-то «плюсы» у C++ не особо видны. В смысле, суть не объектно-ориентированная, а вполне такая процедурная.

            Only users with full accounts can post comments. Log in, please.