The information in this chapter augments Chapter 2, Persistence, in the ObjectStore C++ API User Guide. The material is organized in the following manner:
Example of loss of pointer validity outside transaction
#include <ostore/ostore.hh> #include "part.hh" void f() { objectstore::initialize(); static os_typespec part_type("part"); part *a_part_p = 0; employee *an_emp_p = 0; os_database *db1 = os_database::open("/thx/parts"); OS_BEGIN_TXN(tx1,0,os_transaction::update) a_part_p = (part*) ( db1->find_root("part_root")->get_value() ); /* retrieval */ . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2,0,os_transaction::update) an_emp_p = a_part_p->responsible_engineer; /* INVALID! */ . . . OS_END_TXN(tx2)db1->close();}
#include <ostore/ostore.hh> #include "part.hh" main() { objectstore::initialize(); static os_typespec part_type("part"); part *a_part_p = 0; employee *an_emp_p = 0; os_database *db1 = os_database::open("/thx/parts"); OS_BEGIN_TXN(tx1,0,os_transaction::update) a_part_p = (part*) ( db1->find_root("part_root")->get_value() ); /* retrieval */ . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2,0,os_transaction::update) a_part_p = (part*) ( db1->find_root("part_root")->get_value() ); /* re-retrieval */ an_emp_p = a_part_p->responsible_engineer; /* valid */ . . . OS_END_TXN(tx2) db1->close(); }
To use pvars, you define the variable you want to hold the pointer to the entry point. Then you pass the variable's address to the function os_pvar::os_pvar(), along with the name of the root that points to the desired entry point object, and a pointer to the database containing the root.
This function is the constructor for the class os_pvar, but you never have to explicitly use the instance of os_pvar that results. This instance must be a stack object, however, so do not create it with new.
Once you have called the os_pvar constructor, ObjectStore automatically maintains an association between the variable and the entry point. At the beginning of each transaction in the current process, if the database containing the specified root is open, ObjectStore establishes a valid pointer to the entry point object as the value of the variable. It also sets the variable to point to the entry point when the database becomes open during a transaction.
When control leaves the block in which the os_pvar constructor was called, the destructor for the resulting instance of os_pvar is executed, breaking the association between the variable and entry point. Therefore, if you are using the ObjectStore transaction macros, you should call the constructor from outside any transaction, since the macros establish their own block.
See Type Safety for Database Roots in Chapter 2 of the ObjectStore C++ API User Guide for more information on the use of typespecs.
Pvar Example
Here is an example of the use of pvars:
#include <ostore/ostore.hh> #include "part.hh" void f() { objectstore::initialize(); static os_typespec part_type("part"); part *a_part_p = 0; employee *an_emp_p = 0; os_database *db1 = os_database::open("/thx/parts"); os_pvar p(db1, &a_part_p, "part_root", &part_type); OS_BEGIN_TXN(tx1,0,os_transaction::update) . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2,0,os_transaction::update) an_emp_p = a_part_p->responsible_engineer; /* valid */ . . . OS_END_TXN(tx2) db1->close(); }Note that, even though you can use this variable from one transaction to the next without re-retrieving its value, you cannot use it in between transactions. As always, you must be within a transaction to access persistent data. In between transactions, ObjectStore automatically sets the variable to 0. The variable is also set to 0 during a transaction if the database containing its associated root is closed.
Note also that you should not try to set the value of this variable, since ObjectStore handles all assignment of values to it. If you want to retrieve different objects at different times through the use of a single pvar, use a pvar to associate a pointer-valued variable with a pointer-valued entry point object. Then you can change what the entry point points to as needed.
This function will be executed upon the call to os_pvar() or at the beginning of subsequent transactions, if the database to contain the root is open and ObjectStore cannot find the specified root in that database. It will also be called when this database becomes open during a transaction, and ObjectStore cannot find the root in that database.
os_pvar( os_database *db, void *location, char *root_name, os_typespec *typespec = 0, void *(*init_fn)(os_database*) = 0 );ObjectStore provides three standard initialization functions, os_pvar::init_long(), os_pvar::init_int(), and os_pvar::init_pointer(). These each allocate an object of the appropriate type (long, int, or void*), initialize the object to 0, and return a pointer to it. You can supply either a standard or a user-defined function to the os_pvar constructor.
#include <ostore/ostore.hh> #include "part.hh" void *part_init(database *db) { static os_typespec part_type("part"); return new(db, part_type) part("part_0"); } void f() { objectstore::initialize(); static os_typespec part_type("part"); part *a_part_p = 0; employee *an_emp_p = 0; database *db1 = database::open("/thx/parts"); os_pvar p(db1, &a_part_p, "part_root", &part_type, part_init); OS_BEGIN_TXN(tx1,0,os_transaction::update) a_part_p->display(); . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2,0,os_transaction::update) an_emp_p = a_part_p->responsible_engineer; /* valid */ . . . OS_END_TXN(tx2) db1->close(); }
os_object_cluster* create_object_cluster(os_unsigned_int32 size) ;The function returns a pointer to an os_object_cluster. Unlike segments, however, which are variable sized and expand to accommodate whatever is added to them, clusters have a fixed size. You specify the size in bytes of a new cluster as an argument to create_object_cluster(). This number must be less than 65536, since 64 KB is the maximum cluster size.
os_segment *seg1; . . . os_object_cluster *clust1 = seg1->create_object_cluster(4096) ;The actual size of the new cluster is the result of rounding the specified size up to the next whole number of pages, minus the platform architecture alignment (see ObjectStore Building C++ Interface Applications).
Do not perform create_object_cluster() on the transient segment.
os_database *db1; part *an_old_part, . . . part *a_new_part = new( os_object_cluster::of(an_old_part) , part_type ) part(111);
enum os_fetch_policy { os_fetch_page, os_fetch_segment, os_fetch_stream }; void os_database::set_fetch_policy ( os_fetch_policy, os_int32 bytes); void os_segment::set_fetch_policy ( os_fetch_policy, os_int32 long bytes);Using the set_fetch_policy() function on an os_database object changes the fetch policy for all segments in that database, including segments created by the current process in the future.
Note that a fetch policy established with set_fetch_policy() (for either a segment or a database) remains in effect only until the end of the process making the function call. Moreover, set_fetch_policy() only affects transfers made by this process. Other concurrent processes can use a different fetch policy for the same segment or database.
os_segment *text_segment; /* The text segment contains long strings of characters */ /* representing page contents, which tend to be referred */ /* to consecutively. So tell ObjectStore to fetch them */ /* 16 KB at a time. */ text_segment->set_fetch_policy (os_fetch_page, 16384);
This means that, when you scan a segment sequentially, after the first two transfers from the segment, each transfer from the segment replaces the data cached by the second-to-last transfer from that segment. This way, the last two chunks of data retrieved from the segment will generally be in the client cache at the same time. And, after the first two transfers, transfers from the segment generally will not result in eviction of data from other segments. This policy also greatly reduces the internal overhead of finding pages to evict.
os_segment *image_segment; /* The image segment contains scan lines full of pixel data, */ /* which we're about to traverse in sequence for image */ /* sharpening. Telling ObjectStore to stream the data from */ /* the server in 32 KB chunks gives us access to adjacent */ /* scan lines simultaneously and optimizes client/server traffic. */ image_segment->set_fetch_policy (os_fetch_stream, 32768);When you perform allocation that extends a segment whose fetch policy is os_fetch_stream, the double buffering described above begins when allocation reaches an offset in the segment that is aligned with the fetch quantum (that is, when the offset mod the fetch quantum is 0).
ObjectStore references carry some extra cost over the use of pointers. They are larger than pointers (between 8 and 16 bytes, depending on what kind you use), and dereferencing one usually involves a table lookup.
The most generally useful ObjectStore references are instances of the parameterized class os_Reference. More specialized reference classes are discussed in
#include <ostore/ostore.hh> #include <stdio.h> class employee { static os_typespec *get_os_typespec(); . . . }; class part { static os_typespec *get_os_typespec(); . . . os_Reference<employee> responsible_engineer; . . . }; void f() { objectstore::initialize(); static os_database *db1 = os_database::open("/thx/parts"); static os_database *db2 = os_database::open("/thx/parts"); OS_BEGIN_TXN(tx1, 0, os_transaction::update) part *a_part = new(db1, part::get_os_typespec()) part; employee *an_emp = new(db2, employee::get_os_typespec()) employee; a_part->responsible_engineer = an_emp; printf("%d\n", a_part->responsible_engineer->emp_id); OS_END_TXN(tx1) db1->close(); }Here, the member responsible_engineer is declared to be of type os_Reference<employee>. The class name in angle brackets is the referent type. It indicates that values of responsible_engineer are references to instances of the class employee.
When the employee* (an_emp) is assigned to a_part->responsible_engineer, a reference to this employee* is automatically constructed and stored there. You can use an employee* anywhere a reference<employee> is expected. In general, you can use a T* anywhere a reference<T> is expected, because there is a conversion constructor, os_Reference::os_Reference(T*).
Now you can use the reference to the employee in many contexts requiring an employee*. This is because the class os_Reference overloads the -> operator, and defines a conversion operator so its instances are converted to pointers to instances of its referent type, when appropriate. So you just use the reference as you would a pointer to its referent type, as in the printf statement.
Note that the -> and conversion operators are the only operators with special reference class overloadings, so references do not behave like pointers in the context of other operators, like [] and ++.
Referring Across Transactions
"Example: os_Reference" shows how references can be used to refer from one database to another. References can also be used to refer across transactions.
In an ObjectStore application, you typically retrieve pointers to persistent objects and store them in transiently allocated variables. You can normally use these pointers only in the transaction in which they were retrieved:
Example of transiently allocated (invalid) pointers
#include <ostore/ostore.hh> #include <ostore/coll.hh> #include "part.hh" void f() { objectstore::initialize(); os_Set<part*> *part_set; part *a_part; os_database *db1 = os_database::open("/thx/parts"); OS_BEGIN_TXN(tx1, 0, os_transaction::update) part_set = (os_Set<part*>*) ( db1->find_root("part_set")->get_value() ); /* retrieval */ . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2, 0, os_transaction::update) a_part = part_set->query_pick( /* INVALID! */ "part", "part_number==123456", db1 ); . . . OS_END_TXN(tx2) db1->close(); }Here, part_set is a pointer to an entry point, a set of part* pointers. Since part_set is transiently allocated, its value is valid only until the end of the current transaction. So its use in the query in the next transaction is invalid, and will have unpredictable results. If you want to use this pointer in a subsequent transaction, it must be retrieved again:
#include <ostore/ostore.hh> #include <ostore/coll.hh> #include "part.hh" main() { objectstore::initialize(); os_Set<part*> *part_set; part *a_part; os_database *db1 = os_database::open("/thx/parts"); OS_BEGIN_TXN(tx1, 0, os_transaction::update) part_set = (os_Set<part*>*) ( db1->find_root("part_set")->get_value() ); /* retrieval */ . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2, 0, os_transaction::update) part_set = (os_Set<part*>*) ( db1->find_root("part_set")->get_value() ); /* re-retrieval */ a_part = part_set->query_pick( /* OK */ "part", "part_number==123456", db1 ); . . . OS_END_TXN(tx2) db1->close(); }
#include <ostore/ostore.hh> #include <ostore/coll.hh> #include "part.hh" main() { objectstore::initialize(); part *a_part; os_Reference<os_Set<part*> > part_set_ref; os_database *db1 = os_database::open("/thx/parts"); OS_BEGIN_TXN(tx1, 0, os_transaction::update) part_set_ref = (os_Set<part*>*) ( db1->find_root("part_set")->get_value() ); /* retrieval */ . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2, 0, os_transaction::update) a_part = part_set_ref->query_pick( "part", "part_number==123456", db1 ); /* OK */ . . . OS_END_TXN(tx2) db1->close(); }Here, the variable part_set_ref is declared to be of type os_Reference< os_Set<part*> >. The class name in angle brackets, os_Set<part*>, is the referent type. It indicates that part_set_ref refers to an instance of the class os_Set<part*>. When the os_Set<part*>* returned by the get_value() is assigned to part_set_ref, a reference to the os_Set<part*> is automatically constructed and stored in part_set_ref. You can use an os_Set<part*>* anywhere an os_Reference< os_Set<part*> > is expected.
As described above, you can use a T* anywhere a os_Reference<T> is expected, because there is a conversion constructor, os_Reference::os_Reference(T*).
You can also use the os_Reference in any context requiring an os_Set<part*>. As mentioned above, this is because the class os_Reference overloads the * and -> operators, and defines a conversion operator so its instances are converted to pointers to instances of its referent type, when appropriate. So you just use the reference as you would a pointer to its referent type, as when calling query_pick().
#include <ostore/ostore.hh> #include <iostream.h> void dump_part(part *a_part) { os_Reference<part> part_ref = a_part; cout << part_ref; } os_Reference<part> read_dump() { os_Reference<part> part_ref; cin >> part_ref; return part_ref; }
os_reference is just like os_Reference, except the conversion constructor used is os_reference(void*) instead of os_Reference(T*). In addition, the conversion operator used is operator void*() instead of operator T*(), which means that you should use a cast to pointer-to-referent type when dereferencing an os_reference.
#include <ostore/ostore.hh> #include <stdio.h> class employee { static os_typespec *get_os_typespec(); . . . }; class part { static os_typespec *get_os_typespec(); . . . os_reference responsible_engineer; . . . }; f() { objectstore::initialize(); static os_database *db1 = os_database::open("/thx/parts"); static os_database *db2 = os_database::open("/thx/parts"); OS_BEGIN_TXN(tx1, 0, os_transaction::update) part *a_part = new(db1, part::get_os_typespec()) part; employee *an_emp = new(db2, employee::get_os_typespec()) employee; a_part->responsible_engineer = an_emp; printf("%d\n",(employee*) (a_part->responsible_engineer)->emp_id); OS_END_TXN(tx1) db1->close(); }
#include <ostore/ostore.hh> #include <ostore/coll.hh> #include "part.hh" main() { objectstore::initialize(); part *a_part; os_database *db1 = os_database::open("/thx/parts"); os_reference part_set_ref; OS_BEGIN_TXN(tx1, 0, os_transaction::update) part_set_ref = (os_set*) ( db1->find_root("part_set")->get_value() ); /* retrieval */ . . . OS_END_TXN(tx1) OS_BEGIN_TXN(tx2, 0, os_transaction::update) a_part = (part*) ( ((os_set*) (part_set_ref))->query_pick( "part", "part_number==123456", db1 ) ) ; /* ok */ . . . OS_END_TXN(tx2) db1->close(); }
This means that if you copy a database containing an os_Reference or os_reference, the reference in the copy and the reference in the original might refer to different databases. To change the database a reference refers to, you can use the ObjectStore utility oschangedbref, which is described in oschangedbref: Changing External Database References in ObjectStore Management.
Note that an intradatabase reference (a reference referring to an object in the same database) uses a pathname of the form ../db-name, where db-name is the terminal component of the database's pathname. So if you copy or move this database in such a way as to change the terminal component of its pathname - say by making a copy within the same directory - the reference in the copied or moved database will not be an intradatabase reference; it will refer to the location ../db-name, which has the same terminal component as the original. To make sure you get an intradatabase reference, use os_Reference_this_DB or os_Reference_local. See ObjectStore Lightweight References.
A discussion on how relative pathnames work can be found in Cross-Database Pointers and Relative Pathnames in Chapter 2 of the ObjectStore C++ API User Guide.
ObjectStore Lightweight References
Instances of the class os_Reference take up 12 bytes of storage each. If you are using a large number of references, you might be able to use one of the lightweight reference types to save space.
Local References
Each os_Reference stores a database pathname. An os_Reference_local, in contrast, saves space by not storing the pathname of the referent database, and instead requiring you to supply a pathname when dereferencing it. So you dereference an os_Reference_local explicitly, with a call to the function os_Reference_local::resolve(), which takes a database* argument. Example: use of os_Reference_local
Here is an example:
#include <ostore/ostore.hh> #include <stdio.h> class employee { static os_typespec *get_os_typespec(); . . . }; class part { static os_typespec *get_os_typespec(); . . . os_Reference_local<employee> responsible_engineer; . . . }; f() { objectstore::initialize(); static os_database *db1 = os_database::open("/thx/parts"); static os_database *db2 = os_database::open("/thx/employees"); OS_BEGIN_TXN(tx1, 0, os_transaction::update) part *a_part = new(db1, part::get_os_typespec()) part; employee *an_emp = new(db2, employee::get_os_typespec()) employee; a_part->responsible_engineer = an_emp; printf("%d\n", a_part->responsible_engineer->resolve(db2)->emp_id); OS_END_TXN(tx1) db1->close(); }Just as os_reference is the nonparameterized version of os_Reference, os_reference_local is the nonparameterized version of os_Reference_local. Resolving an os_reference_local is just like resolving an os_Reference_local; no cast is necessary (as is necessary with an os_reference), since resolution is explicit.
There is also a nonparameterized class, os_reference_transient, for referring from transient memory. You use it just as you would os_reference, except that it cannot be allocated in persistent storage.
If you want to use a reference solely for the purpose of saving on virtual memory addresses, you can use an os_Reference_this_DB. When ObjectStore resolves one of these references, it assumes the referent is in the same database as the reference itself. So, even though these references save space by not storing a database pathname, they do not have to be resolved explicitly. You use an os_Reference_this_DB just as you would an os_Reference.
There is also a nonparameterized class os_reference_this_DB, which you use just as you would os_reference.
The protected reference classes are
Each time you create a protected reference, a write to the database is performed, to maintain a persistent table that associates protected references with their referents.
You can test a safe reference to see if its referent has been deleted with the member function deleted().
A summary of the various ObjectStore reference classes is presented in Summary of ObjectStore Reference Types.
Summary of ObjectStore Reference Types
Parameterized reference classes
The table below summarizes the characteristics of the various parameterized ObjectStore references.
Nonparameterized reference classes
The table below summarizes the characteristics of the various nonparameterized ObjectStore references.
Retaining Pointer Validity Across Transactions
If you are converting an existing application to use ObjectStore, it might be inconvenient to rewrite the code to use references, especially if the application will use many short transactions. ObjectStore has a feature that enables you to retain persistent addresses across transaction boundaries, so that it is not necessary to use references.
The static member function objectstore::release_persistent_addresses() globally releases all persistent addresses. After this function is called, all existing transient-to-persistent pointers are invalidated.
You can determine whether persistent addresses are currently retained with objectstore::get_retain_persistent_addresses(), which returns nonzero for true and 0 for false.
Example: retrieving persistent addresses
Here is an example:
#include <ostore/ostore.hh> #include "part.hh" main() { objectstore::initialize(); os_database *db1; . . . person *p, *q; . . . objectstore::retain_persistent_addresses(); OS_BEGIN_TXN(tx1,0,os_transaction::update) p = (person *) (db1->find_root("fred")->get_value()); /* Now p is valid, and can be referenced normally. */ p->print_info(); OS_END_TXN(tx1) /* p cannot be dereferenced outside a transaction, but it */ /* can be stored anywhere. */ q = p; OS_BEGIN_TXN(tx1,0,os_transaction::update) /* If persistent addresses were not retained, we */ /* could not do this without using references for p and q */ q->print_info(); OS_END_TXN(tx1) }
Consider the following union:
union myunion { struct { int i; foo * fptr; } S1 ; struct { bar * btr; float f; } S2 ; } ;ObjectStore sometimes has the task of modifying all pointers embedded in an object when the object is brought into memory (see ObjectStore Memory Mapping Architecture in Chapter 1, ObjectStore Concepts, of the ObjectStore C++ API User Guide). In the case above, such an object will contain a pointer either at offset 0 or at offset 4 bytes, depending on whether S1 or S2 is the correct interpretation of the object's structure.
Because the application can reconfigure the object (switch between S1 and S2) at will, the application must record the state of the object relative to layout, and provide a functional interface for extracting that information. This functional interface is the discriminant function.
Defining and using unions with discriminant functions
The union above can be modified slightly to accommodate this as follows:
union myunion { int Tag; struct { int T; int i; foo * fptr; } S1 ; struct { int T; bar * btr; float f; } S2 ; os_int32 discriminant(); } ; os_int32 myunion::discriminant() { return Tag; }Applications that use this union type must take care to do
S1.T = 1;when they want to use the S1 layout.
Likewise, when they want to use the S2 layout, they should do
S2.T = 2;Switching layouts must be accompanied by reassigning the leading integer.
class myclass { public: myunion MyU; int Tag; os_int32 discriminant(); }; os_int32 myclass::discriminant() { return Tag; }Now applications must take care that the Tag records the value of the union layout. For example:
myclass MyC; MyC.Tag = 1; MyC.S1.i = 0; /* . . . later on we switch */ MyC.Tag = 2; MyC.S2.f = 1.1;In the first example, the name of the discriminant function, myunion::discriminant, serves to associate the function with the union myunion. In the second example, the name myclass::discriminant serves to associate the function with the one and only union-valued data member of myclass. When a class has more than one union-valued data member, the name of each discriminant function should have the form
discriminant_union-name_data-member-namewhere union_name is the name of the associated union, and data-member-name is the name of the associated data member.
For heterogeneity considerations, in the ObjectStore C++ API Reference, Chapter 5, User-Supplied Functions, see Discriminant Functions for additional details.
Updated: 03/31/98 15:27:23