[Scummvm-devel] File handling revamp
Max Horn
max at quendi.de
Tue Jul 29 01:38:39 CEST 2008
Hi folks,
as mentioned on this list and elsewhere a few times in the past, I
think our file handling system needs an overhaul. We already
introduced the FSNode stuff some time ago to try to improve the
situation with regards to abstracting the notion of paths, but that's
obviously not enough.
Let me list the problems I perceive right now, and some proposal as to
how to address this. As always, suggestions and constructive criticism
is highly welcome. Sorry, this mail is quite long, but that just shows
how bad I think the File handling code really is ... ;).
* Lack of documentation, both for implementors/porters, and for users.
-> Solution: Add more documentation :). I feel responsible for that,
but need help. In particular, I'll need people to read whatever I
fabricate (not just spelling, but also whether it is sufficiently
detailed). But of course everyone's welcome to contribute to the docs.
* No good error handling, in particular, no clear error handling
semantics. We provide a bool File::ioFailed() method, and also an
eof() method, but we do not specify when exactly it will be set, and
by which methods.
-> Solution: Adapt the ferror/clearerr semantics. They might not be
the best in the world, but they are already explicitly defined and
well-documented. People can just look at "man ferror", POSIX specs
etc. if something is not clear in our docs. Writing good doxygen
comments gets easier, too.
* The overly convoluted, complicated, under-documented and abused
File::addDefaultDirectory() system (The blame is on me to a big
extent, of course :). Semantics are unclear here, too. Right now, we
blindly add every directory in our reach and it's friends, and then
just hope the files we want are included somewhere there. And that
there are no name clashes. No way of saying "game data files are in
these dirs, GUI theme stuff please only from that dir". One specific
problem: The AdvancedDetector's detectGame() function needs to support
taking either a FSList param, or alternatively uses File::open() to
locate files. Besides complicating the code, this causes a disparity
between detecting and running games, which can lead to subtle bugs.
-> Solution: Scrap it! And replace it with something better. Right now
I am not sure what *exactly* that means; we first will have to compile
a list of requirements. I.e., what exactly are those search paths used
for?. Then come up with a design proposal based on that.
One approach might be to introduce a SearchDirectory class, which is
inited with a FSNode, and could cache the list of all files in it (and
optionally of its subdirs, recursively). Then either File::open
(optionally) takes (a list of) SearchDirectory object(s), and/or a
global set of active SearchDirectories could be stored. This approach
makes it easy to dynamically add/remove dirs from the search list
(just dump the SearchDirectory object); or to search for files only in
a subset of all search dirs. Not to mention that we get proper OO-
style code encapsulation.
* Path separators (slashes) are silently creeping into File::open
calls, decreasing portability in a fashion that currently is hard to
detect.
-> Solution: Change the code to use FSNodes as it should, or implement
the SearchDirectory proposal above, or something else which makes it
possible to conveniently load files from a (set of) dirs.
Also: add a check to File::open() that warns (ultimately: errors out)
when a slash is used in a file name.
* We have a very complex File::open method (did I already mention that
we don't document the semantics of stuff anywhere? ;).
It has to do quite a lot: Handling search paths, trying to ignore
case, fall back solution for alternate file locations, etc.. This
leads to a very complex behavior, which can once more lead to subtle
bugs.
-> Solution: Split it into two parts:
1) Locate the file, via search paths & directory FSNodes (which makes
it possible to add support for case insensitivity in a much more
logical and portable way), but also via port specific fallback options
(like for Mac OS X & iPhone right now).
2) Open the file. See also below.
* On ports which do not support FILE, or where FILE is bugged, porters
have to use #define tricks to get class File to work. The DS, PS2 and
Symbian ports do just that, see common/file.cpp
-> Solution: Make it possible to provide alternate File-class
implementations, just like for FSNodes. One way would be by adding a
FileManager class, with internal methods like
SeekableReadStream *openFileForReading(String name);
SeekableReadStream *openFileForReading(FSNode);
WriteStream *openFileForWriting(String name);
and then the File class simply calls these in open(), and otherwise
calls-through to the stream object it obtained.
* Right now, the default Savefile also makes use of FILE. Same
problems as above. We could change it to either use class File (which
we currently don't, to avoid the nastiness that is called File::open).
Or we could use the above FileManager class
* Writing to files: This is not really a serious issue with our
current system, but something that contributes its share to the
complexity of class File and to the generic portability issues. I am
aware of the following reasons why we *write* data to files:
1) save states (handled via savefile managers)
2) config file
3) debug/resource dumps, screenshots, stuff like that
4) MT-32 emu cache; GUI theme cache; other cache files?
Well, for 1) we already provide dedicated code (the savefile manager).
For 2) we badly need to provide dedicated code (see next point), and 3
is purely optional. So, in short, ports could let openFileForWriting()
return a dummy object which does nothing, if they don't support
writing to arbitrary files.
* The config file right now is load & saved via class File. On the
Dreamcast, saving is disabled using #ifdef. The PS2 port generates a
fake special path to be able to handle the config file in a special
way. Maybe other ports to extra trickery as well.
In addition, lots of ports provide special #ifdef'ed code to determine
the config file location. Nasty. Yet another place porters have to be
aware of.
-> Solution: Add another set of FileManager methods:
SeekableReadStream *openConfigFileForReading();
WriteStream *openConfigFileForWriting();
The default implementation could look like it does now, but the DC,
PS2 and other ports could easily customize it. No more complicated
tricks!
* File buffering: Recently some ports were bitten by the fact that
lots of our code rlies on file buffering. If you look at line 205 in
common/file.cpp, you'll also notice a
setvbuf(file, NULL, _IOFBF, 8192);
(for __amigaos4__ only).
Well, we should document this need; we should have such an setvbuf on
all systems where it is supported and makes sense. By letting backends
override the file code in a more flexible way, we hopefully also make
it easier to implement read buffering. If desired, we could even try
to provide some generic read buffering code.
That's all for now. But I guess the list is long enough. Though I am
sure I forgot stuff. Porters, speak up if you can think of something
else that'd help! Engine maintainers and everybody else, speak up if
you have concerns missing on this list!
Some more random rambling before I close:
* The more I think about it, the more I like the idea of having a
pure virtual FileManager class (to be implemented by ports, with a
default implementation based on FILE), which provides some
minimalistic core API for creating File/DumpFile/ConfigFile/SaveFile
streams. The point being that a port could use the exact same
implementation for all these types of files, or use a completely
custom one for each.
Also, if we decide that we still need a global set of
SearchDirectories, then it might be useful to let the FileManager
manage them.
* We could even merge the SaveFileManager into the FileManager, though
I haven't really thought much about that, it's just a wild idea; the
current system works well, so I would only change it if it provides a
benefit to us, as such a change will force our porters to change an
otherwise working system.
* For those who haven't noticed: One goal I have in mind for ScummVM
is to move *all* port specific code into backends/, and not use any
#ifdef tricks in our main code base. This may not be 100% possible,
but I'd like to get as close as possible. It's just so much easier to
create a new port when you only have to look into backends/ to see
what all you have to do. Oh, and when things you have to implement are
properly documented.... ah, what a nice dream :)
* As a first immediate (and temporary) step, I propose to split the
reading and the writing part of our existing class File. The attached
patch does that. This is only temporary, and simplifies the code in
class File a bit, enabling further refactoring. Whether we keep the
split between File and DumpFile or eventually merge them back together
remains to be seen, though right now I am in favor for keeping it.
Cheers,
Max
-------------- next part --------------
A non-text attachment was scrubbed...
Name: dumpfile.patch
Type: application/octet-stream
Size: 31205 bytes
Desc: not available
URL: <http://lists.scummvm.org/pipermail/scummvm-devel/attachments/20080729/1dd814fb/attachment.obj>
-------------- next part --------------
More information about the Scummvm-devel
mailing list