Object mutation
Although it is very powerful, object replacement may not cover all the needs you may have. Sometimes, when loading objects, it is necessary to mutate them. This will happen mainly due to differences between the class definition of a newly loaded object and the class definition currently in the image. In general, there are two cases where object mutation is necessary:
• Mutation of current objects in the image due to loading in new class definitions
• Mutation of newly loaded objects whose class definitions do not match with the definition in the image where they are being loaded
The first case is currently not supported by the Swapper because this case occurs mainly during the development phase and is handled by the VA Smalltalk Basic development environment. The second case, which includes class definition changes and renaming of a class, is fully supported by the Swapper.
Mutation of loaded instances
When objects are dumped, there is no guarantee that they will be loaded in an image with the same class definitions. Some classes might be missing, and others might have changed, such as instance variables added or removed. For example, suppose in release 1.0 of WidgetApplication we define the classes WidgetA and WidgetB as follows:
Object subclass: #WidgetA
instanceVariableNames: 'e f g h i'
classVariableNames: "
poolDictionaries: "
Object subclass: #WidgetB
instanceVariableNames: 'w x y z'
classVariableNames: "
poolDictionaries: "
Suppose also that in release 1.1 of WidgetApplication we redefine the class WidgetA and rename the class WidgetB to be WidgetC as follows:
Object subclass: #WidgetA
instanceVariableNames: 'g j h i e f'
classVariableNames: "
poolDictionaries: "
Object subclass: #WidgetC
instanceVariableNames: 'z y x q'
classVariableNames: "
poolDictionaries: "
When instances of WidgetA from release 1.0 are loaded into the image using the release 1.1 definition, the mutation process, by default, will be handled by the Behavior instance method mutateObjects:fromPlatform:named:withDefinition: provided by the Swapper. The default behavior of the mutation code is to map all the old instance variable name slots to the corresponding instance variable name slots in the new definition. If a new instance variable name is added to a class definition in the image and there is no corresponding instance variable name in the old definition, then the mapping provided by the class will be used (that is, Behavior instance method defaultInstanceVariableMappings). If a mapping is not provided (that is, nil) or the mapping does not specify what the new instance variable name slot should contain, then nil will be put in that new variable name slot.
This means that classes that may need to mutate instances of old versions into valid instances of the new version will have to implement the methods above. These methods can be difficult to implement because they have to be aware of all past formats and representations of a class.
Requirements for object mutation
In order to properly check whether the representation of a class in the image is compatible with the one the object had when unloaded, an ObjectDumper has to be configured with the includeInstVarNames value set to true. This causes it to include information about instance variable names, which can be checked against the information in the image where the objects are being loaded.
However, if the objects were unloaded by a Swapper version prior to V3.0 or unloaded by the Swapper V3.0 with the includeInstVarNames value set to false, information about names of instance variables was not dumped. The objects (when loaded) will be mutated into instances of the class in the image with the same name, without performing any checking. It is assumed that the class in the image with the same name is valid and compatible with the one when the object is dumped.
Loading objects whose instance variable names have been renamed
If an instance variable in a class is renamed (e.g. instance variable w of WidgetB of release 1.0 is renamed to be instance variable q of WidgetB in release 1.1) and you do not specify the mapping between the instance variables w and q, then the value of w is discarded when an instance of WidgetB is loaded, and the value of q is set to nil. By overriding the default behavior of the Behavior instance method defaultInstanceVariableMappings, you can specify the mapping of w in the old definition to q in the new definition. For example, consider the case where instances of WidgetB from release 1.0 are loaded into an image containing WidgetB release 1.1 definition, before the class was renamed to WidgetC. In order to map the instance variable w to q, define WidgetB class method defaultInstanceVariableMappings as follows.
Example: Mapping instance variables of different versions of a class
defaultInstanceVariableMappings
"Answer a Dictionary whose keys are the new instance variable
names and whose values are one-parameter blocks."
^Dictionary new at: 'q' put: ([:anArray|
(anArray at: 1) instVarAt: ((anArray at: 3) at: 'w')]);
yourself
Any class which overrides the default
Behavior instance method
defaultInstanceVariableMappings must answer an instance of
Dictionary. The keys in the
Dictionary are the new instance variable names that are not in the old class definitions. Their corresponding values are one-parameter blocks (that is, instances of
BlockContextTemplate). Each block when evaluated should expect a four-element
Array as its parameter. The four elements in the
Array are as follows. (
3)
• An Array whose contents are the instance variables of the old instance
• The class name of the old instance (that is, the class may have been renamed)
• A Dictionary which maps instance variable names to instance variable positions in the definition of the old instance
• A String which represents the name of the platform from which the old instances were unloaded
Loading objects whose classes have been renamed
If a class is renamed (for example, WidgetB is renamed to WidgetC), then unloaded instances of that class cannot be reloaded into the image unless you provide an association that maps the original class name to the new class name. For example, instances of WidgetB cannot be loaded into the image directly, because the class WidgetB has been renamed to WidgetC. The ObjectLoader provides two protocols which allow unloaded instances of WidgetB to be loaded and converted into instances of WidgetC: ObjectLoader instance methods addMutatedClassNamed:newClass: and removeMutatedClassNamed:. The example below shows how to load and convert an instance of WidgetB into an instance of WidgetC.
Example: Defining mutation for objects to be loaded
| widgetC loader stream|
(stream := CfsReadFileStream open: 'widgetb.swp')
isCfsError ifTrue: [
self error: stream printString].
stream isBytes: true.
"Makes sure DBString will never be returned as result of #next:, etc."
(loader := ObjectLoader new)
addMutatedClassNamed: 'WidgetB' newClass: WidgetC.
widgetC := loader loadFromStream: stream.
loader removeMutatedClassNamed: 'WidgetB'.
widgetC inspect.
File widgetb.swp is assumed to contain a dumped instance of WidgetB.
Note:
Since Version 4.0, it has been possible to define a mutation entry (WidgetA > WidgetB) even if both classes are present in the image. This was not possible in previous releases.
Overriding default object mutation code
The main purpose of the Behavior instance method mutateObjects:fromPlatform:named:withDefinition: is to provide a generic mutation algorithm that will handle the most general case of object transformation. Classes that have undergone several definition changes may require mutation code which is more specific to their particular situations. These classes can override the generic mutation code to handle their own transformation of old instances when they are loaded. For example, suppose in release 1.2 of WidgetApplication, the class WidgetA is redefined to be as follows:
Object subclass: #WidgetA
instanceVariableNames: 'g i h e f'
classVariableNames: ''
poolDictionaries: ''
When we load instances of WidgetA from release 1.0 and 1.1, we want to perform the following transformations.
Table 1. Defining transformation and object mutation
| |
| |
| |
e > | e |
d > | f |
g > | g |
h > | h |
i > | i |
| |
e > | e |
f > | f |
g > | g |
h > | h |
i > | (discarded) |
j > | i |
| |
Since the generic mutation code cannot handle the mutation case where instances of WidgetA from release 1.1 are loaded into an image that contains a release 1.2 definition, WidgetA must, therefore, provide the class method mutateObjects:fromPlatform:named:withDefinition: in order to override the one in Behavior. The next example shows how to do this.
Example: Defining mutation code for classes
mutateObjects: oldObjects
fromPlatform: platformString
named: oldClassName
withDefinition: oldDefinition
"Use the default mutation code to handle the usual transformation.
If the oldDefinition does NOT include variable 'j' then no other
changes are required by special code. Otherwise, remap the instance
variable 'j' of each old object to instance variable 'i' in the
corresponding new object. Answer an Array of new WidgetAs."
| newObjects oldObject newObject newInstVarNames indexOfI |
newObjects := super mutateObjects: oldObjects
fromPlatform: platformString
named: oldClassName
withDefinition: oldDefinition.
(oldDefinition includesKey: 'j')
ifFalse: [^newObjects].
newInstVarNames := self allInstVarNames.
indexOfI := newInstVarNames indexOf: 'i'.
1 to: oldObjects size do: :index|
oldObject := oldObjects at: index.
newObject := newObjects at: index.
newObject instVarAt: indexOfI put:
(oldObject instVarAt: (oldDefinition at: 'j'))
].
^newObjects
WidgetA class method mutateObjects:fromPlatform:named:withDefinition: is responsible for mapping the old instance variable name slots into the new ones. In cases where a class must implement its specific mutation code, it must follow the protocol specification in Behavior instance method mutateObjects:fromPlatform:named:withDefinition:.
-----
Footnotes:
An Array is used in Versions 4.0 and 4.5 for compatibility with previous releases.