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:
The first case is currently not supported by the Swapper because this case occurs mainly during the development phase and is handled by the IBM Smalltalk development environment. The second case, which includes class definition changes and renaming of a class, is fully supported by the Swapper.
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.
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.
![]() | Since the framework is implemented in Smalltalk, you can redefine this behavior simply by defining a new loading replacer. For details, see Replacers and strategies as first class objects. |
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
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. |
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
WidgetA |
|
---|---|
Objects to be loaded | Current definition in image |
Release 1.0 | Release 1.2 |
e > | e |
d > | f |
g > | g |
h > | h |
i > | i |
Release 1.1 | Release 1.2 |
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:.