Charles is a system designer who can be reached on CompuServe 71230, 1217, BIX crovira, or care of Adelphi, 3465 Wyman Crescent, Gloucester, Ontario, Canada K1V OP3.
I recently had to develop several expert systems that, along with a host of other considerations, had to be able to use files created by existing applications. The expert system then needed to store the data from those files in other files that could be shared on a network. This article describes a technique that I used for building those requirements into my expert systems. This technique, which uses persistent objects, allows programs to access files from other applications and then store shareable persistent objects on disk.
Among the other constraints on this project were the type of PCs at my disposal (a network of XT clones), user-interface considerations, and a limited amount of time available to create the system. Consequently, I decided that the best development environment for the job was Smalltalk -- in particular, Digitalk's Smalltalk/V.
Why is an XT-based system being discussed in a magazine that focuses on Macintosh programming? Because I do all of my work -- code development, documentation creation, the writing of user's guides, the works -- on a Macintosh, using Smalltalk/V Mac. I develop applications on my Mac at home, copy them to a Compaq portable, and then port them to PCs at the client site in Smalltalk/V or Smalltalk/V286. This process is convenient for two reasons: The PC can be used effectively with a decent user interface, and I don't have to recode my applications (apart from niggling details).
Smalltalk is the language that defined all of the relevant concepts of object-oriented programming and provided the first implementation of that approach. Fundamentally, Smalltalk remains a single-user, memory-based, single processor system. Furthermore, Smalltalk's file-handling system is quite limited, satisfying all of its needs with Streams of bytes.
Most programmers never see beyond this limitation. During the course of their programming experiences, they've developed the attitude that any language without unit-record support can't be considered a real computer language. This common misconception stems from the fact that languages are generally fixed, and thus are limited by their original definition. Because file I / 0 is usually the territory of compiler writers, we programmers are stuck with whatever file-handling capabilities are already provided for us in a language. What can be a revelation to programmers about Smalltalk is that this program can be a superb file manager or database manager, if the appropriate objects -- such as persistent objects -- are defined.
In general, persistent objects can be thought of as files that consist of collections of bytes, one after another, that are normally read from beginning to end as Streams of data. Streams may be positioned at any point in the stream, thereby providing random access. The file-management system built into Smalltalk handles Streams quite well.
In the simplest kind of persistent object, files are just repositories for fixed-format data. Each file contains only one type of record. The files are completely external to memory, and a window, one record wide, is provided for viewing the files, as illustrated in Figure 1.
The process of extending Streams from files of bytes to files of unit records is relatively simple. Listing One (page 74) provides all of the code needed to implement a mechanism to access a file of any record type. This code is safe to use, as long as you don't position, read, or write by any other methods than these methods implemented in class RecordStream, which have not been disinherited. It is still possible to position the file at points other than at record boundaries. (If you write off boundaries, you will destroy your file.) This implementation also has no end-of-file handling mechanism in its file reads, and currently passes end-of-file detection to the atEnd message that the file inherits from class File. The code is primitive, but usable.
The definition of RecordStream incorporates a frequently used Smalltalk concept -- the notion of a model. In a "model," objects manipulate other objects. By providing the manipulative objects with the means to interrogate any manipulated object about information relevant to a manipulative object, it is possible to both make an object perform actions and to cause the object to act upon any other object.
For example, say we need to read from and write to a file of employee data in which fields are delimited by commas, and records are delimited by carriage returns. We can define a model for employee records. The Employee class should define all of the behavior of an employee, from the date of hire to the date of termination. Listing Two (page 74) contains a sample class definition for employees. This Employee model enables us to create an employee file, albeit a very simple one. Let's say that the employee master for January 1989 is called EmpMast.891. Access to the file could be performed in this way:
| empfile empRec |
empfile := File pathName:
'EmpMast.891' model: EmployeeWe can read the fifth employee record by sending the recordReadAt: message to the file, and passing the record number as a parameter: empRec := empfile recordReadAt: 5.
We read the next employee record with the code: empRec := empfile recordReadNext.
We can write the fifth employee record by sending the recordWrite:at: message to the file, and passing the employee to be written and that employee's record number as the following parameters: empfile recordWrite: empRec at: 5.
We read the next employee record by sending the recordWriteNext: message to the file, and passing the employee to be written as a parameter: empfile recordwriteNext: empRec.
Now that we have provided a basic mechanism for dealing with flat files, the unit record mechanism can be extended to include structured files such as dBase files, B-tree files, and other allocation and indexing schemes. The use of a record model class now allows us to define a unit record file as a file that contains something.
A more interesting type of persistent object -- the persistent collection -- allows you to store instances of objects, regardless of their class or size. This type of persistent object is implemented by extending the collection classes beyond the bounds of memory. When such an object is accessed, the collection is loaded into memory, and the instances that make up the collection remain on disk.
With persistent collections, files can contain variable-length data, and they are no longer limited to one class of object per file. The files are partially resident in memory, and a window on the entire file is provided. Only the specific instances within the collection are disk-resident, as shown in Figure 2.
The position of the class within the hierarchy also means that a fast-running development version of an application can be produced quickly. You can easily modify both test and production versions of the application in order to use external storage. To do so, just change the initialization and termination methods of the application.
The use of persistent collections lets you create and maintain objects that can be orders of magnitude larger than the memory available to most computers (640K in the PC world, and 1 Mbyte in the Macintosh world.) Most real-world data is larger than a computer's available RAM memory, and these classes enable Smalltalk to aspire to real-world applications on PCs.
Listings Three and Four (page 74 and 76, respectively) contain a sample class definition for PersistentArray. The file is accessed in an initialization routine:
| empPerArr empRec |
empPerArr := PersistentArray
open: 'EmpPA.891' of: 10.After initialization, PersistentArrays are totally transparent and can be used in the same way as any other array. To access the fifth instance, send the array the message: empRec := empPerArr at: 5. To add or update the fifth instance, send the array the message: empPerArr at: 5 put: empRec. This definition of PersistentArray includes only three methods for managing the array: at:, at:ifAbsent:, and at:put:.
The rest of the definition contains methods for managing the file-resident portion of PersistentArray. These methods handle the following activities:
Of course, extending the flexibility of Smalltalk outside of its own memory does not come without certain risks and costs. The synchronization between the memory-resident copy and the instances on disk may be corrupted if one process updates the object while another process is accessing the object, or if a failure to return the memory-resident portion of a persistent collection to disk occurs (because of a system crash, a software error, or whatever).
Also, a collection may no longer fit in the space reserved for it at the front of the file. The file may contain large gaps, and may need garbage collection, a simple but time-consuming activity. The entire process and the persistent object structure can be optimized in order to minimize the need for garbage collection.
Persistent objects and persistent collections allow access to existing data and provide the flexibility of Smalltalk, while enabling you to store objects on disk and even to share objects between applications. The synchronization of views across operating system task boundaries can be accomplished with some judicious intertask messaging. The entire problem can also be circumvented through the use of a server-client mechanism, which, of course, presents its own subtleties.
Persistent Object Tools. Knowledge Systems Corporation. Suite 270, 2000 Regency Parkway, Cary, NC 27511-8507, 919-481-4000.
Maier, David and Stein, Jacob. "Development and Implementation of an Object-Oriented DBMS" Research Directions in Object-Oriented Programming, Shriver, Bruce and Wegner, Peter (eds.), [Boston: MIT Press, 1987 ISBN: 0-262-19264-0, pp. 355-392.
Rovira, Charles-A. "Sequence Intolerance in Expert Applications." AI Expert 4:4 (April 1989): ISSN 0888-3785, pp. 56-59.
_PERSISTENT OBJECTS_
by Charles-A. Rovira
[LISTING ONE]
Copyright © 1989, Dr. Dobb's Journal
" *************************************************************************
This system has been developped for Digitalk's Smalltalk/V. It was
developed in /V Mac, ported to /V and tested in /V and /V Mac.
Author: Based on some preliminary code by Steve Northover.
Packer: This file, documentation and additional methods by Charles-A.
Rovira.
Install: fileIn this file.
Globals: none.
startUp: none required.
shutdown: all files should be closed. None enforced.
Usage: See notes below.
Class(es): Object, File, RecordStream
Class: Object
Method(s): recordToString: stringToRecord: recordSize
Class: File
Method(s): path:model:
Class: RecordStream
Method(s): model: positionAt:
recordReadAt: recordReadNext
recordWrite:at: recordWriteNext
These routines implement a file management system comparable to
the COBOL random/sequential file system.
In order to use unit-record files if is necessary to create a class
which will respond to at least three messages:
The first two are class methods:
recordSize, which answers the size of unit-record on the file..
stringToRecord, which translates a string loaded from disk into a
Smalltalk internal representation of an object instance
and answers an new instance of the modeled object
the third is an instance method:
recordToString, which translates the Smalltalk internal representation
of an object instance into a string to be stored on disk
The model class can manipulate the records in whatever manner is
appropriate to the application in addition to these methods. The
system in a bit fool-proofed in that the Object class defines
a simple version of these three methods. For IBM card-image files
it is only necessary to define a class for manipulating or extracting
information from or putting information onto the card images.
Nota Bene: Files run from 1 to n record. They are not zero based
Examples:
A unit record file can be accessed as follows:
| turf "TemporaryUnitRecordFile"
turfRec "TemporaryUnitRecordFile-Record" |
turf := File pathName: '<file>' model: AnExampleClass
A unit record file can be read as follows:
turfRec := turf recordReadAt: anInteger.
Sequential reading is performed as follows:
turfRec := turf recordReadNext.
A unit record file can be written to as follows:
turf recordWrite: turfRec at: anInteger
Sequential writing is performed as follows:
turf recordwriteNext: turfRec.
*************************************************************************** "
!Object methods !
recordToString
"Private - Lowest level of unit-record file management.
We assume that the object defining the content of the
unit-record file will store its string image That's
what's answered.
This should be implemented in a unit-record model SubClass"
^self storeString! !
!Object class methods !
stringToRecord: aString
"Private - Lowest level of unit-record file management.
We assume that the object defining the content of the
unit-record file is filled by a string image. In order.
to provide for more flexibility we evaluate the string.
That's what's answered.
This should be implemented in a unit-record model SubClass"
^Compiler evaluate: aString! !
!Object class methods !
recordSize
"Private - Lowest level of unit-record file management.
We assume that the record length will be 80 bytes as
that has been the standard size to assume for over a
century.
This should be implemented in a unit-record model SubClass class"
^80! !
!File class methods !
pathName: aString model: aClass
"Answer a RecordStream. This is the entry point
of the unit-record file management system. Access files
by specifying the name of the file to use and the class
which models the type of objects the file should contain"
| anArray dir file aDirectory aStream |
"The following code has been duplicated from the Directory class
to simplify creation of the RecordStream class "
anArray := self splitPath: aString.
dir := anArray at: 1.
file := anArray at: 2.
aDirectory := Disk.
dir = '' ifFalse: [
dir first = $: ifTrue: [
dir := aDirectory pathName,
(dir copyFrom: 2 to: dir size)]].
"The preceeding code was been duplicated from the Directory class
to simplify creation of the RecordStream class "
aStream := RecordStream on: (File open: file in: aDirectory).
^aStream
model: aClass! !
FileStream subclass: #RecordStream
instanceVariableNames:
'model'
classVariableNames: ''
poolDictionaries: '' !
!RecordStream class methods ! !
!RecordStream methods !
model: aClass
"Private - Set the model for the objects in the file.
Used only by the pathName:model: in class File "
model := aClass!
positionAt: anInteger
"Position the receiver before the object at anInteger.
Unit-Record files run from 1..n not 0..n
while not strictly speaking a private method it really
seves no real purpose outside of the recordReadAt: and
recordWrite:at: methods in this (ReadStream) class"
self position: (anInteger - 1 * model recordSize)
recordReadAt: anInteger
"Answer the unit accessible by the receiver at
anInteger position in the file. Report an error if
the receiver stream is positioned at the end. "
self positionAt: anInteger.
^self recordReadNext!
recordReadNext
"Answer the next record accessible by the receiver
and advance the stream position. Report an error if
the receiver stream is positioned at the end."
| bytes |
self atEnd ifTrue: [
^self error: 'Read beyond end of file'].
bytes := String new: model recordSize.
CursorManager write showWhile: [
1 to: model recordSize do: [:i |
bytes at: i put: self next]].
^model stringToRecord: bytes!
recordWrite: anObject at: anInteger
"Position the receiver before the object at anInteger.
and write the object onto the file"
self positionAt: anInteger
self recordWriteNext: anObject!
recordWriteNext: anObject
"Write anObject to the receiver stream. Report an error
if its too big and pad with spaces if its too small"
| bytes size |
bytes := anObject recordToString.
bytes size > model recordSize
ifTrue: [^error: 'record too big'].
CursorManager write showWhile: [
self nextPutAll: bytes.
self next: (model recordSize - bytes size) put: $ .]! !
[LISTING TWO]
" *************************************************************************
A sample class to describe the behavior of any instance of an
Employee object.
*************************************************************************** "
Object subclass: #Employee
instanceVariableNames:
'lastName firstName socInsNum'
classVariableNames: ''
poolDictionaries: '' !
! Employee class methods !
recordSize
"we will assume that employees each have:
20 characters for the lastName
1 character for the comma
20 characters for the firstName
1 character for the comma
9 characters for the SocInsNum (social insurance number)
1 character for the Carriage return/Line feed"
^52
stringToRecord: aString
"Answer a new Employee object"
^Employee new initializeWith: aString!
! Employee methods !
initializeWith: aString.
"Fill the receiver from the string"
| aStream |
aStream := aString asStream.
firstName := aStream upTo: ',' ;skip: 1.
lastName := aStream upTo: ',' ;skip: 1.
socInsNum := aStream upTo: CrLf.
recordtoString: anEmployee
"answer a string for writing to disk"
^firstName, ',' lastName, ',', socInsNum , '\' withCrs! !
[LISTING THREE]
" *************************************************************************
Extending Collections in Smalltalk/V. The PersistentArray
Install: Class Loader must be installed (See DL/4 in CIS AIExpert forum)
fileIn this file.
Globals: Class variable: OpenInstances.
startUp: PersistentArray initialize
Install in SystemDictionary start-up
shutdown: PersistentArray shutdown.
In /V Mac, regenerate ShutDownList
Usage: See notes below.
Class(es): PersistentArray
Class: PersistentArray
Method(s): Class -
initialize
open:of:
open:of:readOnly:synchronized:
shutdown:
Instance -
=
associate:with:
associationsDo:
at:
at:ifAbsent:
at:put:
close
closeReadOnlys:
coerce
compress:
contents
contents:
do:
file:
fileAppend:
fileHeader
fileReadAt:
fileReadSizeAt:
fileRemoveAt:
fileReplace:at:
fileWrite:of:at:
includes:
initHeader
loadBy:
loadFrom:as:and:
loadHeader
readOnly
removeAt:notFound:
reserve:
synchronize:
synchronized
unloadBy:
These routines extend collections outside the boundaries of Smalltalk
memory.
In order to use PersistentArray it is only necessary to create an
instance of this class by issuing the class message 'open'. It can
then be used like any other Array until you want to dispose of it.
Then it must be closed.
Nota Bene: The loader class must allready be present.
Examples:
A unit record file can be accessed as follows:
| tpa |
tpa := PersistentArray open: '<file>' of: 10 .
Access to and from the array is the same as for any other array.
tpa at: 5.
tpa at: 5 put anObject
Instances can be removed by setting them to nil or by explicitely
requesting a deletion:
tpa at: 5 put: nil - or -
tpa removeAt: 5 notFound: [].
Saving the object to disk is accomplished by:
| tpa |
tpa close.
*************************************************************************** "
Object subclass: #PersistentArray
instanceVariableNames:
'content file lostBytes headSize readOnly synchronized appendsCoerced '
classVariableNames:
'OpenInstances '
poolDictionaries:
'CharacterConstants ' !
!PersistentArray class methods !
initialize
"Private - there are no OpenInstances, Make it so."
OpenInstances := OrderedCollection new.!
open: aFileName of: anInteger
"Open the persistent object in read/Write mode"
| temp |
temp := super new.
temp initialize: anInteger; loadFrom: aFileName as: false and: false.
^temp!
open: aFileName of: anInteger readOnly: aBoolean synchronized: aBoolean2
"Open the Persistent Array
in whatever mode
and whatever synchronization"
| temp |
temp := super new.
temp initialize: anInteger; loadFrom: aFileName as: aBoolean and: aBoolean2.
^temp!
shutdown: aBoolean
"Private - close OpenInstances"
OpenInstances do: [:each |
each close ]! !
!PersistentArray methods !
= aPersistentArray
"Quickie to compare PAs"
^ (file pathName = aPersistentArray file pathName)!
associate: aKey with: aPosition
"return an association for internal use"
^Association key: aKey value: aPosition!
associationsDo: aBlock
"Answer the receiver. For each element in the receiver,
evaluate aBlock with that element as the argument."
| index element |
index := super size.
[index > 0]
whileTrue: [
(element := super at: index) == nil
ifFalse: [aBlock value: self fileReadAt: element value].
index := index - 1]!
at: anInteger
"Answer the value of the key/value pair at anInteger.
If not found, report an error."
^ self at: anInteger ifAbsent:[self errorAbsentElement]!
at: anInteger ifAbsent: aBlock
"Answer the value of the key/value pair at anInteger
If not found, evaluate aBlock (with no arguments)."
| answer |
^ (answer := content at: anInteger) == nil
ifTrue: [aBlock value]
ifFalse: [(self fileReadAt: answer) value]!
at: anInteger put: anObject
"Answer the object.
If setting to nil
remove the object."
| old |
(anObject == nil)
ifTrue: [^self removeAt: anInteger
notFound: [self error: 'Persistent Array boundaries']].
(old := content at: anInteger) == nil
ifFalse: [content at: anInteger
put: (self fileReplace: (self associate: anInteger
with: anObject)
at: old)]
ifTrue:[content at: anInteger
put: (self fileAppend: (self associate: anInteger
with: anObject))].
^anObject!
close
"closing the persistent object
if its readOnly
close it
if its readWrite
close all readOnlys on it
see if it needs to be compressed
update the header
pull it off the OpenInstances OrderedCollection"
| aStream newSize |
CursorManager execute showWhile: [
readOnly
ifTrue:
[file close]
ifFalse:
[self closeReadOnlys: self.
aStream := WriteStream on: ''.
Loader new unload: content on: aStream.
newSize := (2 + content size) * 32.
(lostBytes > (file size /4) or:
[headSize < newSize])
ifTrue:
[self compress: newSize].
CursorManager write showWhile:
[self fileHeader.
file flush;
close]].
OpenInstances remove: self]!
closeReadOnlys: aPersistentObject
"close read only objects tied to this read/write aPersistentObject"
OpenInstances do: [:each |
((each = aPersistentObject) and:
[each readOnly ])
ifTrue: [each close]]!
coerce
"the read/write instance has appendsCoerced"
appendsCoerced := true!
compress: newKs
"copies objects referenced by the old dictionary onto
a new dictionary"
| window newPersistent newName |
GrafPort push.
window := Window dialogBox: (20 @ 50 extent: 450 @ 150).
'Compressing a Persistent Object...'
displayAt: 2 @ 2 * SysFont charSize
font: Font menuFont.
'CAUTION:'
displayAt: 2 @ 5 * SysFont charSize.
'Please do not interrupt this process with Control-Break'
displayAt: 5 @ 6 * SysFont charSize.
" not much to this, is there?"
newName := file pathName.
newPersistent := PersistentArray open: (newName, '$$$')
of: (2 * content size).
newPersistent reserve: newKs.
self associationsDo: [:anOldAssociation |
newPersistent at: anOldAssociation key
put: anOldAssociation value].
newPersistent file close.
file close.
File remove: newName.
File rename: (newName,'$$$')
to: newName.
file reOpen.
window release.
GrafPort pop.!
contents
"Answer the contents for this instance"
^content!
contents: anArray
"This read only object has its indexes updated"
content := anArray!
do: aBlock
"Answer the receiver. For each value in the receiver,
evaluate aBlock with that value as the argument."
content do: [:each |
each isNil ifFalse: [
aBlock value: (self fileReadAt: each) value]]!
file
"Answer the file for this instance"
^file!
fileAppend: anAssociation
"Answer a position
find a logical end of file
derive the size of the unloaded assoclation
write the resultiing collection and size at the end"
| aPosition aStream aSize |
readOnly ifTrue: [
^self error: 'You cannot update this object'].
CursorManager execute showWhile: [
aPosition := file size max: headSize.
aStream := WriteStream on: ''.
Loader new unload: anAssociation on: aStream.
aSize := aStream collection size].
^self fileWrite: aStream collection of: aSize at: aPosition!
fileHeader
"write the header information to disk"
| temp |
CursorManager write showWhile: [
temp := Array with: headSize
with: lostBytes
with: content.
file position: 0.
Loader new unload: temp on: file]!
fileReadAt: aPosition
"Answer an Association.
read the size of and the association"
| size |
CursorManager read showWhile: [
file position: aPosition.
size := (Loader new loadFrom: file) "asInteger".
^Loader new loadFrom: file]!
fileReadSizeAt: aPosition
"Answer the size.
read the size of the association which follows"
CursorManager read showWhile: [
file position: aPosition.
^(Loader new loadFrom: file) asInteger]!
fileRemoveAt: aPosition
"Lose the bytes"
self readOnly
ifTrue: [^self error: 'You cannot update this object'].
lostBytes := lostBytes + self fileReadSizeAt: aPosition.!
fileReplace: anAssociation at: aPosition
"Answer aPosition or the logical end of file
derive the size of the unloaded association
look up the old size on disk
if its still fits
write it in place
else
find the logical end of file
append it"
| aStream newSize oldSize aNewPosition |
"(anAssociation isKindOf: Association)"
readOnly
ifTrue: [^self error: 'You cannot update this object'].
aStream := WriteStream on: ''.
Loader new unload: anAssociation on: aStream.
newSize := aStream size.
oldSize := self fileReadSizeAt: aPosition.
(oldSize < newSize or: [appendsCoerced])
ifTrue:
[lostBytes := lostBytes + oldSize.
aNewPosition := file size max: self header.
^self fileWrite: aStream collection
of: newSize
at: aNewPosition]
ifFalse:
[lostBytes := lostBytes + (oldSize - newSize).
^self fileWrite: aStream collection
of: oldSize
at: aPosition].!
fileWrite: aCollection of: bytes at: aPosition
"write anAssociation size and anAssociation"
CursorManager write showWhile: [
file position: aPosition.
Loader new unload: bytes printString
on: file.
file nextPutAll: aCollection.
"file nextPutAll: '%'."
file flush].
^aPosition!
includes: anObject
"Answer true if the receiver contains the key/value
pair whose value equals anAssociation, else answer false."
self do: [ :element |
(self fileReadAt: element) value = anObject
ifTrue: [^ true]].
^ false!
initHeader
"initialize the persistent object header"
headSize := (2 + content size) * 32.
lostBytes := 0.
self reserve: headSize.!
initialize: anInteger
"initialize the persistent object collection"
content := Array new: anInteger!
loadBy: aLoader
"Write out the instance variables of the receiver
using the loader object aLoader."
self error: 'Can''t (un)load a Persistent object (its pointless)'!
loadFrom: aFileName as: readOnlyStatus and: synchronizedStatus
"initialize or load the Persistent object
as readWrite [check for reopening of readWrite] or
readOnly [as
requiring appendsCoerced or
requiring synchronization] "
| temp |
file := (File pathName: aFileName).
(readOnly := readOnlyStatus)
ifFalse: [OpenInstances do:
[:each | (each file pathName = file pathName and:
[each readOnly = false])
ifFalse: [
^self error: 'Cant open: ',aFileName, ' twice for update']]]
ifTrue:
[File primitiveChangeModeOf: aFileName to: 1.
"Change to read only mode. # from 'Inside Macintosh'"
appendsCoerced := synchronizedStatus not.
(synchronized := synchronizedStatus)
ifFalse:
[self openInstances do: [:each |
each file pathName = file pathName and:
[each readOnly not]]
ifFTrue: [each coerce]]].
OpenInstances add: self.
file size = 0
ifTrue:
[self initHeader]
ifFalse:
[self loadHeader].
^self!
loadHeader
"get the persistent object header from disk"
| temp |
CursorManager read showWhile: [
file position: 0.
temp := (Loader new loadFrom: file).
headSize := temp at: 1.
lostBytes := temp at: 2.
content := temp at: 3]!
readOnly
"Answer the readOnly for this instance"
^readOnly!
removeAt: anInteger notFound: aBlock
"Answer anInteger. Remove the key/value pair at anInteger
If such a pair is not found, evaluate aBlock
(with no arguments)."
(content at:anInteger) == nil
ifTrue: [^ aBlock value].
self fileRemoveAt: (content at: anInteger)
content at: anInteger put: nil.
^anInteger!
reserve: newSize
"Answer newSize.
Reserve newSize bytes for the persistent collection.
Pad out the file as required."
| fileSize |
headSize := newSize.
fileSize := file size.
1 to: (newSize-fileSize) do: [:junk | file nextPut: Space "$!"].
^newSize!
synchronize: aPersistentObject
"find the read instance(s) and synchronize keys"
self openInstances do: [:each |
each = aPersistentObject and:
[each readOnly and:
[each synchronized]]
ifTrue: [each contents: aPersistentObject contents]]!
synchronized
"answer synchronized"
^synchronized!
unloadBy: aLoader
"Write out the instance variables of the receiver
using the loader object aLoader."
self error: 'Can''t (un)load a Persistent object (its pointless)'! !
[LISTING FOUR]
" *************************************************************************
Extending Collections in Smalltalk/V. The Loader
This system has been developed from Digitalk's Smalltalk/V loader.
The Loader, as defined in Smalltalk/V, required two modifications
to operate in the /V Mac environment.
The original /V Loader class is on the AIExpert forum on CompuServe
in Data Library 4.
Install: fileIn this file.
Globals: none.
startUp: none
shutdown: none
Usage: Internal to Persistent objects.
*************************************************************************** "
Object subclass: #Loader
instanceVariableNames:
'stream loaderIndex objectNumber loader loaderQueue classDict '
classVariableNames: ''
poolDictionaries:
'CharacterConstants ' !
!Loader class methods ! !
!Loader methods !
classIndexFor: aClass
"Unloading - Answer the string for the class of the next
object to be unloaded."
| index |
index := classDict at: aClass
ifAbsent: [
classDict at: aClass put: objectNumber.
^aClass name].
^'%',index printString!
getClass
"Loading - Answer the class of the next object in the file"
| classString char index |
(char := stream next) == $%
ifTrue: [^(loader at: (stream upTo: Lf) asInteger) class].
classString := (String with: char) , (stream upTo: Lf).
^Smalltalk at: classString asSymbol!
getSize
"Loading - Answer the next object size"
^(stream upTo: Lf) asInteger!
load: anObject
"Loading - Load the next object from the file"
| index |
index := 1.
anObject class isPointers
ifTrue: [
[index <= loaderIndex]
whileTrue: [
anObject instVarAt: index put: self nextInstVar.
index := index + 1].
^self].
anObject class isBytes
ifTrue: [
[index <= loaderIndex]
whileTrue: [
anObject at: index put: stream next asciiValue.
index := index + 1].
stream next.
^self].
anObject class isWords
ifTrue: [
[index <= loaderIndex]
whileTrue: [
anObject at: index put:
stream next asciiValue * 256 + stream next asciiValue.
index := index + 1].
stream next.
^self]!
loaderIndex
"Loading - Indicates the size of the next object in the file.
Unloading - Used as an object reference pointer"
^loaderIndex!
loadFrom: aStream
"Loading - Load objects from aStream and return root"
| numOfObjects index anObject |
stream := aStream.
numOfObjects := (stream upTo: Lf) trimBlanks asInteger.
loader := Array new: numOfObjects.
index := 1.
[index <= numOfObjects]
whileTrue: [
loader at: index put: String new.
index := index + 1].
index := 1.
[index <= numOfObjects]
whileTrue: [
anObject := self loadInstance.
(loader at: index) become: anObject."become: (loader at: index). ? -> backwards ?"
(loader at: index) loadBy: self.
index := index + 1].
loader do: [:each| each rehash].
^loader at: 1!
loadInstance
"Loading - Create an empty instance of the next object
in the file"
| class |
class := self getClass.
loaderIndex := self getSize.
class isVariable
ifTrue: [^class basicNew: loaderIndex - class instSize]
ifFalse: [^class basicNew]!
nextInstVar
"Loading - Answer the next instance variable from the stream"
| char ptr answer classString size |
char := stream next.
char == $%
ifTrue: [
ptr := stream upTo: Lf.
ptr = 't' ifTrue: [^true].
ptr = 'f' ifTrue: [^false].
ptr = 'n' ifTrue: [^nil].
^loader at: ptr asInteger].
char == $-
ifTrue: [^(stream upTo: Lf) asInteger negated].
char isDigit
ifTrue: [^((String with: char), (stream upTo: Lf)) asInteger].
char == $$
ifTrue: [
answer := stream next.
stream next.
^answer].
char == $#
ifTrue: [^(stream upTo: Lf) asSymbol].
char == $!!
ifFalse: [
classString := (String with: char),
(stream upTo: Lf).
size := classString size.
(size > 6
and: [(classString copyFrom: size - 5 to: size) = ' class'])
ifTrue: [
classString := classString
copyFrom: 1 to: size - 6.
^(Smalltalk at: classString asSymbol) class].
^Smalltalk at: classString asSymbol].
stream peek == $!!
ifTrue: [
answer := (Compiler evaluate: stream nextChunk)
fileInFrom: stream]
ifFalse: [answer := Compiler evaluate: stream nextChunk].
^answer!
stream
"Answer the stream of the receiver"
^stream!
unload: anObject
"Unloading - unload anObject to the stream"
| size index |
size := anObject class instSize + anObject basicSize.
stream
nextPutAll: (self classIndexFor: anObject class);
nextPut: Lf;
nextPutAll: size printString;
nextPut: Lf.
index := 1.
anObject class isPointers
ifTrue: [
[index <= size]
whileTrue: [
stream nextPutAll: (self unloadIndexFor:
(anObject instVarAt: index)).
index := index + 1.
stream nextPut: Lf].
^self].
anObject class isBytes
ifTrue: [
[index <= size]
whileTrue: [
stream nextPut: (anObject at: index) asCharacter.
index := index + 1].
^stream nextPut: Lf].
anObject class isWords
ifTrue: [
[index <= size]
whileTrue: [
stream nextPut: ((anObject at: index) // 256) asCharacter.
stream nextPut: ((anObject at: index) \\ 256) asCharacter.
index := index + 1].
^stream nextPut: Lf]!
unload: anObject on: aStream
"Unload anObject on aStream"
| class oldPos |
oldPos := aStream position.
(anObject isKindOf: Behavior)
ifTrue: [^self error: 'cannot have ', anObject name, ' as root'].
class := anObject class.
(class == UndefinedObject
or: [class == Symbol
or: [class == Character
or: [class isKindOf: Boolean]]])
ifTrue: [^self error: 'cannot have ', class name, ' as root'].
stream := aStream.
loaderIndex := 1.
objectNumber := 1.
loader := IdentityDictionary new.
classDict := IdentityDictionary new.
loaderQueue := OrderedCollection new.
stream
nextPutAll: ' ';
nextPut: Lf.
loader at: anObject put: 1.
loaderQueue addLast: anObject.
[loaderQueue isEmpty]
whileFalse: [
loaderQueue removeFirst unloadBy: self.
objectNumber := objectNumber + 1].
aStream
position: oldPos;
nextPutAll: ((loaderIndex printString , ' ')
copyFrom: 1 to: 5);
flush!
unloadIndexFor: anObject
"Unloading -- Answer the external string representation for
anObject used in the unload stream."
| tempInt |
(anObject isKindOf: Behavior)
ifTrue: [^anObject name].
anObject == nil
ifTrue: [^'%n'].
anObject == true
ifTrue: [^'%t'].
anObject == false
ifTrue: [^'%f'].
(anObject isKindOf: Integer)
ifTrue: [^anObject printString].
anObject class == Character
ifTrue: [^String with: $$ with: anObject].
anObject class == Symbol
ifTrue: [^'#',anObject].
tempInt := loader at: anObject
ifAbsent: [
loaderIndex := loaderIndex + 1.
loaderQueue addLast: anObject.
loader at: anObject put: loaderIndex].
^'%', tempInt printString! !
!Object methods !
loadBy: aLoader
"Load the instance variables of the receiver using
the loader object aLoader."
aLoader load: self!
unloadBy: aLoader
"Write out the instance variables of the receiver
using the loader object aLoader."
aLoader unload: self! !
!SortedCollection methods !
unloadBy: aLoader
"Write out the instance variables of the receiver
using the loader object aLoader. Convert the
receiver to an OrderedCollection since blocks
of code cannot be loaded or unloaded."
self asOrderedCollection unloadBy: aLoader! !
!String methods !
loadBy: aLoader
"Load the instance variables of the receiver using
the loader object aLoader."
| aStream index size |
size := aLoader loaderIndex.
aStream := aLoader stream.
index := 1.
[index <= size]
whileTrue: [
self at: index put: aStream next.
index := index + 1].
aStream next!
unloadBy: aLoader
"Write out the instance variables of the receiver
using the loader object aLoader."
aLoader stream
nextPutAll: (aLoader classIndexFor: self class);
nextBytePut: 10;
nextPutAll: self basicSize printString;
nextBytePut: 10;
nextPutAll: self;
nextBytePut: 10! !
!Object methods!
rehash
"Rehash the receiver. the default is do nothing."! !
!Set methods!
rehash
"Rehash the receiver."
| aSet |
aSet := self species new: self basicSize.
self do: [ :element | aSet add: element].
^self become: aSet! !
!Dictionary methods!
rehash
"Rehash the receiver."
| aDictionary |
aDictionary := self class new: self basicSize.
self associationsDo: [ :anAssociation |
aDictionary add: anAssociation].
^self become: aDictionary! !
!IdentityDictionary methods!
rehash
"Rehash the receiver."
| aDictionary |
aDictionary := self species new.
self associationsDo: [ :anAssociation |
aDictionary add: anAssociation].
^self become: aDictionary! !