POST++ uses default constructors for initializing object while loading
from storage. Programmer should include macro
CLASSINFO(NAME, FIELD_LIST)
in definition of any class, which
instances can be saved in the storage. NAME
corresponds to the
name of this class. FIELD_LIST
describes reference fields of this
class. There are three macros defined in file
classinfo.h for describing references:
REF(x)
REFS(x)
VREFS(x)
List of these macros should be separates by spaces:
REF(a) REF(b) REFS(c)
.
Macro CLASSINFO
defines default constructor (constructor without
parameters) and declares class descriptor of this class. Class descriptor
is static component of the class with name self_class
.
So class descriptor of the class foo
can be accessed by
foo::self_class
. As far as constructors without arguments
are called for base classes and components automatically by compiler,
you should not worry about calling them explicitly. But do not forget
to include REGISTER(NAME)
. Class names
are placed in the storage together with objects. Mapping between application
and storage classes is established during storage opening. Names of all classes
stored in the storage are compared with names of application classes.
If some class name is not found within application classes or
correspondent application and storage classes have different size, then
program assertion will fail.
These rules are illustrated by the following example:
struct branch { object* obj; int key; CLASSINFO(branch, REF(obj)); }; class foo : public object { protected: foo* next; foo* prev; object* arr[10]; branch branches[8]; int x; int y; object* childs[1]; public: CLASSINFO(foo, REF(next) REF(prev) REFS(arr) VREFS(linked)); foo(int x, int y); }; REGISTER(1, foo); main() { storage my_storage("foo.odb"); if (my_storage.open()) { my_root_class* root = (my_root_class*)my_storage.get_root_object(); if (root == NULL) { root = new_in(my_storage, my_root)("some parameters for root"); } ... int n_childs = ...; size_t varying_size = (n_childs-1)*sizeof(object*); // We should subtract 1 from n_childs, because one element is already // present in fixed part of class. foo* fp = new (foo:self_class, my_storage, varying_size) foo(x, y); ... my_storage.close(); } }
It is up to the programmer whether to use explicit or implicit memory
deallocation. Explicit memory deallocation is faster
(especially for small objects) but implicit deallocation (garbage collection)
is more reliable. In POST++ mark and sweep garbage collection scheme is used.
There is special object in the storage: do_garbage_collection
attribute to
storage::open()
method). It is also possible to explicitly
invoke garbage collection during program execution by calling
storage::do_mark_and_sweep()
method. But be sure that there are
no program variable pointed to objects inaccessible from the root objects
(these objects will be deallocated by GC).
Because of multiple inheritance C++ classes can have non zero offset
within object and references inside object are possible. That is why
we have to use special technic to access object header.
POST++ maintains page allocation bitmap each bit of which corresponds to
the page in the storage. If some large object is allocated
at several pages, then bits corresponding to all pages occupied by this object
except first one will be set to 1. All other pages have correspondent bits in
bitmap cleared. To find start address of the object, we first align pointer
value on the page size. Then POST++ finds page in bitmap that contains
beginning of the object (this page should have zero bit in bitmap).
Then we extract information about the object size from object header placed
at the beginning of this page. If size is greater than half of page size then
we have already found object descriptor: it is at the beginning of the page.
Otherwise we calculate fixed block size used for this page and round down
offset of pointer within this page to block size. This scheme of
header location is used by garbage collector, operator delete
defined in object
class and by methods extracting information
from the object header about object size and class.
In POST++ special overloaded new
method is provided
for allocation of objects in the storage. This method takes as extra
parameters class descriptor of created object, storage in which object
should be created and, optionally, size of varying part of the object instance.
Macro new_in(STORAGE, CLASS)
provides
"syntax sugar" for persistent object creation.
Persistent object can be delete by redefined operator delete
.
object
class defined in object.h.
This class contains no variables and provides methods for object
allocation/deallocation and obtaining
information about object class and size at runtime. It is possible to
use object
class as one of multiple bases of inheritance
(order of bases is not significant). Each persistent class should
have constructor which is used by POST++ system (see section
Describing object class).
That means that you should not use constructor without parameters for
normal object initialization. If your class constructor even has no
meaningful parameters, you should add dummy one to distinguish your
constructor with constructor created by macro CLASSINFO
.To access objects in persistent storage programmer needs some kind of root object from which each other object in storage can be accessed by normal C pointers. POST++ storage provides two methods allowing you to specify and obtain reference to the root object:
void set_root_object(object* obj); object* get_root_object();When you create new storage
get_root_object()
returns NULL.
You should create root object and store reference to it by
set_root_object()
method. Next time you are opening storage,
root object can be retrieved by get_root_object()
.
Hint: In practice application classes used to be changed during
program development and support. Unfortunately POST++ due to its simplicity
provides no facilities for automatic object conversion (see for example
lazy object update scheme in
GOODS),
So to avoid problems with adding new fields to the objects, I can recommend
you to reserve some free space in objects for future use. This is especially
significant for root object, because it is first candidate for adding new
components. You should also avoid reverse references to the root object.
If no other object has reference to the root objects, then root object
can be simply changed (by means of set_root_object
method)
to instance of new class. POST++ storage provides methods for setting
and retrieving storage version identifier. This identifier can be used
by application for updating objects in the storage depending on the storage
and the application versions.
'~'
is used in non-transaction mode
for temporary copy of the file, name preceding with symbol '.'
is use for transaction log, and name preceding with symbol '#'
is used only in Windows-95 in non-transaction mode for backup version
of the original file.
Two other parameters of storage constructor have default values.
First of them max_file_size
specifies limitation of
storage file extension. If storage file is larger than
storage::max_file_size
then it will not be truncated but
further extends are not possible. If max_file_size
is
greater than the file size, then behavior depends on storage opening mode.
In transaction mode, file is mapped on memory with read-write protection.
Windows-NT/95 extends in this case size of the file till
max_file_size
. The file size will be truncated by
storage::close()
method to the boundary of last object allocated
in the storage. In Windows it is necessary to have at least
storage::max_file_size
free bytes on disk to successfully
open storage in read-write mode even if you are not going to add new objects
in the storage. In copy_on_write_map
mode, file is mapped on
memory with copy on write protection and extra segment of memory is allocated
beyond end of the file. When storage::flush()
operation is issued,
file mapping segment and used portion of the extension segment are copied to
the temporary file, which is then renamed to the original one.
The last parameter of storage constructor is max_locked_objects
,
This parameter is used only in transaction mode to provide buffering
of shadow pages writes to the transaction log file. To provide
data consistency POST++ should guaranty that shadow page will be
saved in the transaction log file before modified page will
be flushed on disk. POST++ use one of two approaches:
synchronous log writes (max_locked_objects == 0
)
and buffered writes with locking of pages in memory. By locking
page in the memory, we can guaranty that it will not be swapped out
on disk before transaction log buffers. Shadow pages are written to
the transaction log file in asynchronous mode (with operating system
cashing enabled). When number of locked pages exceeds
max_locked_pages
, log file buffers are flushed on disk
and all locked pages are unlocked. Such approach can significantly
increase transaction performance (up to 5 times under NT). But unfortunately
different operating systems use different approaches to locking pages in
memory.
max_locked_pages
parameter greater than 30, than
POST++ will try to extend process working set to feet your
requirement. But my experiments show that difference in performance
with 30 and 60 locked pages is very negligible.
max_locked_pages
parameter greater than 0, then decision whether to use synchronous or
asynchronous writes to the transaction log file will be taken at
moment of storage class creation. If you want to use benefits of
memory locking mechanism (2-5 times, depending on type of transaction),
you should change owner of your application to root
and
grant set-user-ID
permission:
chmod +s application
.
'.'
).
All following write accesses to this page will not cause page faults.
Storage method commit()
flushes all modified pages on disk and
truncates the log file. storage::commit()
method is implicitly
called by storage::close()
. If fault happened before
storage::commit()
operation, all changes will be undone by coping
modified pages from transaction log to the storage data file. Also all changes
can be undone explicitly by storage::rollback()
method. To choose
transaction based model of data file access, specify
storage::use_transaction_log
attribute for
storage::open()
method.Windows 95 specific: In Windows 95 changing protection of pages of mapped file is not possible. That is why in this system file is loaded in memory and commit operation saves modified pages in file.
Another approach to providing data consistency is based on
copy on write mechanism. In this case original file is not affected.
Any attempt to modify page that is mapped on the file, cause creation
copy of the page, which is allocated from system swap and has read-write
access. File is updated only by explicit call of storage::flush()
method. This method writes data to temporary file (with symbol
~
before the file name) and then renames this file to original
one. So this operation cause an atomic update of the file (certainly if
operating system can guaranty atomicity of rename()
operation).
Attention: If you are not using transactions,
storage::close()
method doesn't flush data in the file. So if
you don't call storage::flush()
method before
storage::close()
all modifications done since last
flush
will be lost.
Windows 95 specific: In Windows 95
rename to existing file is not possible, so
original file is first renamed to the name with preceding symbol
#
, then temporary file started with ~
is renamed
to the original name and finally old copy is removed. So if fault
is happened during flush()
operation and after it you find
no storage file, please do not panic, just look for file started with
#
and rename it to the original one.
Hint: I recommend you to use transactions if you are planning to save data during program execution. It is also possible with copy on write approach but it is much more expensive. Also transactions are always preferable if size of storage is large, because creating temporary copy of file will require a lot of disk space and time.
There are several attributes, which can be passed to storage
open()
method:
support_virtual_functions
attribute
is specified then correction of all objects (by calling default constructor)
will be done.
support_virtual_functions
is specified, then protection of region
is temporary changed to copy on write and conversion of loaded objects takes
place.
storage::commit()
or by storage::rollback()
operations. Method storage::commit()
saves all modified
pages on disk and truncates transaction log, method
storage::rollback()
undo all changes made within this transaction.
map_data_file
flag is not set
(for read and write access). If you are first of all interested in reducing
application startup time then mapping of file is always preferable, because
storage open time will be significantly shorter in this case.
garbage_collection
method returns number of deallocated
objects and if you are sure that you have explicitly deallocate all
unreachable objects, then this number should be zero).
As far as garbage collector modifies all objects in the storage (set mark bit),
relink free objects in chains), running GC in transaction mode can be
time and disk space consuming operation (all pages from the file will be copied
to the transaction log file).
You can specify maximal size for storage files by
file::max_file_size
variable. If size of data file is less
than file::max_file_size
and mode is not read_only
,
then extra size_of_file - file::max_file_size
bytes of
virtual space will be reserved after the file mapping region.
When storage size is extended (because of new objects allocation),
this pages will be committed (in Windows NT) and used. If size of file is
greater than file::max_file_size
or read_only
mode
is used, then size of mapped region is exactly the same as the file size.
Storage extension is not possible in the last case. In Windows I use
GlobalMemoryStatus()
function to obtain information about
actually available virtual memory in the system and reduce
file::max_file_size
to this value. Unfortunately I found no
portable call in Unix which can be used for the same purpose
(getrlimit
doesn't return actual information about available
virtual memory for users process).
Interface to object storage is specified in file
storage.h and implementation can be found in
storage.cxx. Operating system dependent part
of mapped on memory file is encapsulated within file
class,
which definition is in file.h and implementation
in file.cxx.
The only thing that you are needed to use POST++ is library
(libstorage.a
at Unix and storage.lib
at Windows).
This library can be produced by just issuing make
command (there is special MAKE.BAT
for Microsoft Visual C++
which invokes NMAKE
with makefile.mvc
as input).
You can place this library to default library catalog in Unix by
changing INSTALL_DIR
parameter in makefile and doing make
install
. By default INSTALL_DIR
points to
/usr/lib
.
guess
.
There are example of two useful persistent classes: hash table and AWL tree.
Definition of these classes (files
awltree.h,
awltree.cxx,
hashtab.h and
hashab.cxx) is a good examples of creating
libraries for POST++. You can see that there almost no POST specific
code in the implementation of these classes. To test these classes I
create special test program testtree.cxx,
which helps me to find a lot of bugs in POST++. This program is also
included in default make target.
When you will link your application with POST++ library, please do not
forget to recompile comptime.cxx file and include
it in the linker's list. This file is necessary for POST++ to
provide executable file timestamp, which is placed in the storage and
used to determine when application is changed and reinitialization
of virtual function table pointers in objects is required.
Attention! This file should be recompiled each time your are
relinking your application. I suggest you to make compiler to call linker for
you and include comptime.cxx
source file in the list of object
files for the target of building executable image
(see makefile).
handle SIGSEGV nostop noprint pass
. If SIGSEGV signal
is not caused by storage page protection violation, but due to a bug
in your program, POST++ exception handler will "understand" that it is
not his exception and send SIGABRT signal to the self process, which
can be normally catched by debugger.
main
function) with structured exception block.
POST++ provides special macro SEN_ACCESS_VIOLATION_HANDLER()
for this purposes. You should usethis macro after __try {}
block.
So main()
function should look something like this:
main() { __try { ... } SEN_ACCESS_VIOLATION_HANDLER(); return 0; }Be sure that Debugger behavior for this exception is "Stop if not handled" and not "Stop always" (you can check it in Debug/Exceptions menu). In file testrans.cxx you can find example of using structured exception handling.
Look for new version at my homepage | E-mail me about bugs and problems