Object replacement
More advanced users of the Swapper might need to dump objects in different ways. For Version 3.0 and earlier, this mechanism was unlinking and relinking. Since Version 4.0, a new mechanism, replacement, has been available for dumping objects.
Object replacement is a general mechanism that allows an object to be substituted by another during a swapping operation (loading or dumping). In the case of dumping, the object that is being dumped can specify a replacement for itself. Say object A specifies that B replaces it when dumping happens. Then, A itself will never be dumped. B, instead, takes its place.
When loading, the object that is being loaded can specify a replacement for itself. Say object C specifies that D replaces it when loading happens. Thus, C itself is is not loaded into the image; it is replaced by D when it is loaded.
Note:
C is not just loaded conceptually. In fact, it is loaded and as soon as the replacement is performed it becomes garbage, having no references to it. Either the scavenger or the garbage collector will claim it as garbage later.
Of course, both replacements can be combined, so that a combined dumping and loading replacement is achieved: A is replaced by B when dumping, and B is replaced by C when loaded.
The sections that follow explain the different kinds of dumping and loading replacements.
Dumping replacement
Advanced usage of the Swapper may require that objects be replaced during the dump operation. Replacing on dump means that any object can be substituted by another object when it is dumped. Three forms of dumping replacement are supported by the Swapper. The precedence for the replacement options when they are used in combination is, from highest to lowest priority:
Object identity
Instance of a class
Instance variable
Object-identity-based replacement
Object-identity-based replacement allows you to replace specific objects in the unloading process. For each object that you want to replace based on identity, a call must be made. You use the method replaceObject:with: in ObjectDumper. The first parameter is the object to be replaced, and the second parameter is the replacement object. For instance, let's assume you want to replace references to global Smalltalk by another object, of a new class. Let's call this new class GlobalDescriptor, and define it as in the following example:
Example: Possible class definition for GlobalDescriptor
Object subclass: #GlobalDescriptor
instanceVariableNames: 'nameOfGlobal'
classVariableNames: ''
poolDictionaries: ''
You will also need to implement the following methods:
GlobalDescriptor class method forGlobalNamed:
forGlobalNamed: aString
^super new nameOfGlobal: aString
GlobalDescriptor instance method nameOfGlobal:
nameOfGlobal: aString
nameOfGlobal:= aString
GlobalDescriptor instance method nameOfGlobal
nameOfGlobal
^nameOfGlobal
Example: Possible replacement object
A GlobalDescriptor can be used to replace a global variable, where it stores the name of the global in its instance variable nameOfGlobal. After defining this class, all that needs to be done is to define a replacement object (a GlobalDescriptor) for global Smalltalk. The example below shows a possible replacement object.
GlobalDescriptor forGlobalNamed: #Smalltalk asString
Example: Identity-based replacement
Once the new class is implemented, GlobalDescriptor, and the replacement object properly defined, identity-based replacement can be done. The example below shows how dumping can be performed so that all references to Smalltalk will be replaced by references to its replacement object, an instance of GlobalDescriptor.
| stream anOrderedCollection dumper |
(stream := CfsWriteFileStream open: 'no-st.swp' ) isCfsError ifTrue: [
self error: stream printString].
(anOrderedCollection := OrderedCollection new: 10)
add: 'This is a test string';
add: (ByteArray new: 21);
add: 203@485;
add: Smalltalk.
dumper := ObjectDumper new.
dumper
replaceObject: Smalltalk
with: (GlobalDescriptor forGlobalNamed: #Smalltalk asString);
unload: anOrderedCollection intoStream: stream.
stream close.
dumper hasErrorOccurred ifTrue: [
self error: dumper currentErrorString].
Replacing something by nil is, conceptually, the same as unlinking, a feature that was present in previous releases and now is obsolete. This can be accomplished by modifying the code in the example above by replacing the replaceObject:with: statement with the following:
dumper
replaceObject: Smalltalk with: nil.
To undo a replacement configuration, use the ObjectDumper instance method removeReplacementFor:. For example:
dumper
removeReplacementFor: Smalltalk. "Smalltalk will no longer be replaced."
Identity-based replacement may be used to implement such things as replacing references to global objects (variables, in this case Smalltalk) by something else. This mode of operation is typically used when an object is referenced by the object to be unloaded, but you do not actually want to unload the referenced object. Instead, the desired behavior is to rebind to an object already in the image when the dumped object is loaded back.
The loading part of this will be clear when we discuss loading replacements, in Loading replacement.
Identity-based replacement has to be configured for every instance of ObjectDumper used.
Class-based replacement
Class-based replacement allows you to specify that all instances of specified classes will be replaced by possibly new instances of other classes. For each kind of object that you want to replace based on its class, use the ObjectDumper instance method replaceInstancesOf:using:. The first parameter is the class whose instances should be replaced (e.g. Point or Rectangle). The second parameter is a one-parameter block. This block will be evaluated using the original object to be replaced as parameter. The result of the block must be the replacement object.
For example, assume that in a given unload operation you want to replace all instances of Point by instances of Association, which will have the x and y values of the point in the Association's key and value slots. The next example shows how this can be done.
Example: Class-based replacement
| stream anOrderedCollection dumper |
(stream := CfsWriteFileStream open: 'no-point.swp') isCfsError ifTrue: [
self error: stream printString].
(anOrderedCollection := OrderedCollection new: 10)
add: 'This is a test string';
add: 203@485;
add: 1@2.
dumper := ObjectDumper new.
dumper
replaceInstancesOf: Point
using: [:point | Association key: point x value: point y];
unload: anOrderedCollection intoStream: stream.
stream close.
dumper hasErrorOccurred ifTrue: [
self error: dumper currentErrorString].
This code replaces the Point 203@485 by a corresponding Association, 203 >485, and Point 1@2 by a corresponding Association, 1 >2.
Similar to what was shown in Object-identity-based replacement, unlinking all instances of a class can be implemented using replacement. You can achieve this by modifying the code in the above example by replacing the replaceInstancesOf:using: statement with the following:
dumper
replaceInstancesOf: Point using: [:point | nil].
Since this is a common case (the need to replace by nil), the API also accepts nil instead of the block. The same statement in "Example: Class based replacement" above can also be modified as follows:
dumper
replaceInstancesOf: Point using: nil.
If we assume there is a single instance of EsSmalltalkDictionary (Smalltalk), then we can also solve the problem of replacing Smalltalk (presented in Object-identity-based replacement) using the following:
dumper
replaceInstancesOf: EsSmalltalkNamespace
using: [:st | GlobalDescriptor forGlobalNamed: #Smalltalk asString].
To undo a replacement configuration, use ObjectDumper instance method removeReplacementForInstancesOf:. For example:
dumper
removeReplacementForInstancesOf: Point. "Points will no longer be replaced."
Class-based replacement using ObjectDumper instance method replaceInstancesOf:using: has to be configured for every instance of ObjectDumper. However, there is an alternative form of defining class based replacement, which will be applied to all instances of an ObjectDumper used in the image. It makes use of protocols that form the Swapper framework (explained in The Swapper framework). For instance, suppose you want instances of CompiledMethod to be replaced by an intermediate representation, which just carries the class name and selector of the compiled method. And you want this form of replacement to apply to any ObjectDumper used in the image. Simply define CompiledMethod instance method dumpingReplacementForReplacer: as in the following example:
Example: Redefining methods to implement class-based replacement
dumpingReplacementForReplacer: aClassBasedReplacer
"Return a replacement object for the receiver,
being requested by aClassBasedReplacer."
^CompiledMethodDescriptor
forClassNamed: self methodClass name asString
selector: self selector asString
CompiledMethodDescriptor is a class whose instances hold the values for the class name and the selector of the compiled method being represented. For example:
Object subclass: #CompiledMethodDescriptor
instanceVariableNames: 'className methodSelector'
classVariableNames: ''
poolDictionaries: ''
You will also need to define CompiledMethodDescriptor instance methods className:, className, methodSelector:, and methodSelector and class method forClassNamed:selector:.
Normally, you need to define the class method definesClassBasedReplacement as in the example below. However, in this case, CompiledMethod has already implemented this method.
Example: Redefining class behavior for class based replacement
definesClassBasedReplacement
"Returns a Boolean that indicates if the receiver defines class-based
replacement or not."
^true
The parameter aClassBasedReplacer can be ignored now for the sake of simplicity (it is explained in detail in The Swapper framework). The method defined in "Example: Redefining methods to implement class-based replacement" on page *** will cause all instances of CompiledMethod to be replaced (upon unload) by the intermediate representation, CompiledMethodDescriptor.
How this dumping replacement can be replaced again, upon load, will be discussed in Loading replacement (for instance, rebinding the instance of CompiledMethodDescriptor to the corresponding CompiledMethod in the image where it is being loaded). We will also see that the same loading replacement mechanism can be used to perform replacement upon load for both kinds of dumping replacements, discussed in Object-identity-based replacement and in this section.
The reason why just implementing dumpingReplacementForReplacer: is not enough is because there are internal optimizations in ObjectDumper which avoid computing dumping replacements for instances of classes which do not define any replacement at all. Therefore, implementing definesClassBasedReplacement as a class method allows ObjectDumper to know whether the optimization is valid for instances of the class or not.
Instance-variable-based replacement
Instance-variable-based replacement allows you to specify that a given instance variable of a given object (based on its class) will be replaced by something else. For instance, suppose that upon unload you want to replace instance variable x of all Points by something else. For each instance variable you want to replace, you can use ObjectDumper instance methods replaceInstVarIndex:of:using: or replaceInstVarNamed:of:using:.
In the case of replaceInstVarIndex:of:using:, the first parameter is the instance variable index, an Integer. This is the same value you would use to index instance variables using methods instVarAt: and instVarAt:put:. The second parameter is the class whose instances will have their instance variable replaced (for example, Point, Rectangle, and so on). The third parameter is a two parameter block. This block will be evaluated using the original object and the index of the instance variable to be replaced as parameters. The result of the block is the replacement object for that particular instance variable of the object.
For example, let's assume that in a given unload operation you want to replace the first instance variable of Point, its x attribute, by the value of instance variable y, so that all points unloaded will have x and y equal. The example below shows how this can be done.
Example: Instance-variable-based replacement (using indexes)
| stream anOrderedCollection dumper |
(stream := CfsWriteFileStream open: 'xy-point.swp') isCfsError ifTrue: [
self error: stream printString].
(anOrderedCollection := OrderedCollection new: 10)
add: 'This is a test string';
add: 203@485;
add: 1@2.
dumper := ObjectDumper new.
dumper
replaceInstVarIndex: 1
of: Point
using: [:point :instVarIndex | point y];
unload: anOrderedCollection intoStream: stream.
stream close.
dumper hasErrorOccurred ifTrue: [
self error: dumper currentErrorString].
This code will replace instance variable 1 of Point 203@485 by its y value, updating it so that the Point is 485@485 when dumped. Point 1@2 will also be updated, and 2@2 will be dumped. Note that this does not affect the original instance of Point in the image. Only the dumped copy of the object is changed.
Using indexes to identify instance variables has many drawbacks. It violates encapsulation, and is likely to cause problems in many possible situations, such as:
When the image is packaged and a given instance variable of a class is removed
When the class representation is changed so that the order of instance variables changes (for example, y and x instead of x and y) or some instance variables are added before existing ones
For these reasons, a second API is provided. By using ObjectDumper instance method replaceInstVarNamed:of:using: your code will work correctly even in the situations listed above. The following example shows how the same behavior of the above example can be achieved, but using instance variable names instead.
Example: Instance-variable-based replacement (using names)
| stream anOrderedCollection dumper |
(stream := CfsWriteFileStream open: 'xy-point.swp') isCfsError ifTrue: [
self error: stream printString].
(anOrderedCollection := OrderedCollection new: 10)
add: 'This is a test string';
add: 203@485;
add: 1@2.
dumper := ObjectDumper new.
dumper
replaceInstVarNamed: 'x'
of: Point
using: [:point :instVarName | point y];
unload: anOrderedCollection intoStream: stream.
stream close.
dumper hasErrorOccurred ifTrue: [
self error: dumper currentErrorString].
This form will work even if the order of instance variables is changed. If the class does not have an instance variable of the same name, the operation has no effect and returns false.
Note that in the case of replaceInstVarNamed:of:using:, the first parameter is the instance variable name, a String. The second parameter is the class whose instances will have the instance variable replaced (for example, Point, Rectangle, and so on). The third parameter is a two parameter block. This block will be evaluated using the original object and the name of the instance variable to be replaced as parameters. The result of the block is the replacement object for that particular instance variable.
Replacing an object by nil is, conceptually, the same as unlinking. For example, you can modify the replaceInstVarNamed:of:using: in the previous example with the following:
dumper
replaceInstVarNamed: 'x'
of: Point
using: [:point :instVarName | nil].
Since the need to replace by nil is a common case, the API also accepts nil instead of the block. For example, modify the same statement as follows:
dumper
replaceInstVarNamed: 'x';
of: Point
using: nil.
To remove the replacements of all instance variables of a given class, you can use ObjectDumper instance method removeReplacementsForInstVarsOf:, where the parameter is the class. For example, to remove the replacements of any instance variables of Point, you can use the code below:
dumper
removeReplacementsForInstVarsOf: Point.
To undo an instance-variable-based replacement configuration, you can use ObjectDumper instance methods removeReplacementForInstVarIndex:of: or removeReplacementForInstVarNamed:of:. For example:
dumper
removeReplacementForInstVarIndex: 1
of: Point.
or,
dumper
removeReplacementForInstVarNamed: 'x'
of: Point.
Instance-variable-based replacement using ObjectDumper instance methods replaceInstVarIndex:of:using: and replaceInstVarNamed:of:using: has to be configured for every instance of ObjectDumper used. However, there is an alternative form of defining instance variable based replacement, which will be applied to all instances of an ObjectDumper used in the image. It makes use of protocols that form the Swapper framework (explained in The Swapper framework).
For example, suppose you want the first instance variable of Point to be replaced by the point's y value. And you want this form of replacement to apply to any ObjectDumper used in the image. You need to define Point instance method dumpingReplacementForInstVar:forReplacer: to perform the replacement and Point class method definesInstVarReplacement as shown in the next example:
Example: Redefining methods to implement instance-variable-based replacement
dumpingReplacementForInstVar: anInteger forReplacer: anInstVarBasedReplacer
"Return a replacement object for the instance variable anInteger
of the receiver, being requested by anInstVarBasedReplacer."
anInteger == 1
ifTrue: [^self y]
ifFalse: [^super dumpingReplacementForInstVar: anInteger
forReplacer: anInstVarBasedReplacer].
definesInstVarReplacement
"Return true so that the ObjectDumper knows instances of this class
need to be sent a message to compute a replacement."
^true
 
Tips
For performance reasons, the framework makes use of only the API that uses indexes to identify instance variables. Therefore, this feature should be used only when absolutely necessary, preferably in classes unlikely to be changed (that is, instance variables removed or rearranged in terms of order). As usual, the dumping replacement for a given instance variable of an object can be replaced once again upon load. This will be shown in Combining dumping and loading replacement
Loading replacement
Upon load, every object is asked for its loading replacement. This allows virtually any object dumped to be replaced by something else in the image where it is loaded. To specify a new loading replacement, you must define method loadingReplacementForReplacer: in the class whose instances for which you want to have a loading replacement. 2
Unlike the dumping replacements presented in Dumping replacement there is no API in ObjectLoader to override a loading replacement for just one instance of loader. For instance, ObjectLoader new replaceInstancesOf: Point using: [:point | point printString] is not permitted. That is, the replaceInstancesOf:using: method is only defined by ObjectDumper and is not defined by ObjectLoader. A loading replacement for a class will be effective for all instances of ObjectLoader used.
For example, suppose you want to replace all instances of Point in the dumped output by their corresponding print string when you load. To do this, you define Point instance method loadingReplacementForReplacer: and Point class method definesLoadingReplacement as in the example below:
Example: Loading replacement
loadingReplacementForReplacer: aLoadingReplacer
"Return a loading replacement for the receiver,
being requested by aLoadingReplacer."
^self printString
definesLoadingReplacement
"Return true so that the ObjectDumper knows instances of this class
need to be processed on load and will make sure that the ObjectLoader
used will compute a replacement."
^true
In the above example, parameter aLoadingReplacer was ignored. Advanced users can query the replacer and its corresponding ObjectLoader, to decide what the replacement must be (see The Swapper framework). An example is the implementation of Class instance method loadingReplacementForReplacer:.
The above example shows how you can define loading replacements for objects previously dumped. In many cases, it is very useful to have a dumping and loading replacement combined. For instance, object A specifies object B as its dumping replacement, and B specifies C as its loading replacement. This is shown in the next section.
Combining dumping and loading replacement
In Dumping replacement we saw how to specify different kinds of dumping replacements for an object. For many cases, only a dumping replacement or only a loading replacement is enough. For other cases, a combination of the two is desired.
Another look at "Identity-based replacement"
In Object-identity-based replacement, we saw how to specify a dumping replacement for an object based on its identity. However, what if we want to specify a loading replacement for the replacement dumped? For instance, if now we want to load the object dumped in "Example: Identity-based replacement" on page ***, so that the reference to Smalltalk is remapped back in the image where the object is loaded, we must define GlobalDescriptor instance method loadingReplacementForReplacer: as in the next example. Remember to define GlobalDescriptor class method definesLoadingReplacement and have it return true as in "Example: Loading replacement" on page ***.
Example: Specifying a loading replacement for a GlobalDescriptor
loadingReplacementForReplacer: aLoadingReplacer
"Return a valid global variable in this image for the loaded descriptor
defined by the receiver. The replacement is being requested by
aLoadingReplacer. If the global does not exist locally, return nil."
^Smalltalk at: self nameOfGlobal asSymbol ifAbsent: [nil]
The method above will replace the GlobalDescriptor. The original reference to Smalltalk in the image where the object was dumped will be remapped to the local global Smalltalk in the image where the objects are loaded.
Note:
Due to internal optimizations, by default not all loaded objects are asked for their loading replacement. See Limitations for details.
Another look at "Redefining methods to implement class-based replacement"
In "Example: Redefining methods to implement class-based replacement" on page *** all instances of CompiledMethod were replaced by instances of CompiledMethodDescriptor. To have these intermediate representations of CompiledMethods rebound again to their corresponding CompiledMethods in the image where they are loaded, you must define the replacement method CompiledMethodDescriptor instance method loadingReplacementForReplacer:. The next example shows how to do this. Remember to define CompiledMethodDescriptor class method definesLoadingReplacement and have it return true.
Example: Specifying a loading replacement for a CompiledMethodDescriptor
loadingReplacementForReplacer: aLoadingReplacer
"Return a valid CompiledMethod in this image for the descriptor
defined by the receiver. The replacement is being requested by
aLoadingReplacer. If the compiled method does not exist locally,
generate an error."
| class localCompiledMethod |
class := Smalltalk classAt: self className asSymbol ifAbsent: [nil].
class isNil
ifTrue: [ "Generate a loading error. Class not in image.
New code number, domain-specific."
^aLoadingReplacer swapper generateError: 1000 forObject: self].
localCompiledMethod :=
class compiledMethodAt: self methodSelector asSymbol ifAbsent: [nil].
localCompiledMethod isNil
ifTrue: "Generate a loading error. CompiledMethod not
in image. New code number, domain-specific."
^aLoadingReplacer swapper generateError: 2000 forObject: self].
^localCompiledMethod
The combined use of a dumping and loading replacement for CompiledMethod presented here swaps only the minimum information necessary, just enough to rebind the object to an equivalent object in the image where it will be loaded. If you also need to transfer the byte codes of the CompiledMethod, you should have no class based dumping replacement and a loading replacement that will fix the bytecodes upon loading, so that the instance is consistent and can be run in the image where loaded.
Note:
Due to internal optimizations, by default not all loaded objects are asked for their loading replacement. See Limitations for details.
Summary on replacement
The general rule is that if you want to have a loading replacement for an object, you have to make sure its class implements loadingReplacementForReplacer:. If you want to specify dumping a replacement, you must first decide what type of replacement you need. In cases where a combination of dumping and loading replacement is needed (A is replaced by B on dumping, which is replaced by C on loading) you must choose the appropriate dumping replacement and make sure the replacement object belongs to a class that implements a loading replacement as well.
A drawback of implementing dumping and loading replacement by redefining methods of the framework is that there might be cases where two different applications want to define the same replacement selector (loadingReplacementForReplacer:, dumpingReplacementForInstVar:ForReplacer:, or dumpingReplacementForReplacer:) in the same class. In The Swapper framework we explain the notion of replacers, and how to customize the framework so that different replacement methods can be defined in the same class by different applications.
-----
Footnotes:
Except immediates like SmallInteger, true, false, nil, and Character. There are also some loading optimizations, turned on by default, which will make an ObjectLoader compute only replacements for objects that were dumped marked as needing loading replacement. See Limitations for details on how to deal with these optimizations.
Last modified date: 05/19/2020