Understanding object pointers

TL;DR

In the virtual machine, objects are just pointers to data. It is possible to use these pointers to walk through memory and visit all objects. It is also possible to swap all pointers to an object to another object by sending become:. This can be useful for updating a running system, or for temporarily replacing an object by a wrapper. It is also possible to change th class of an object at run time, but this is tricky and should be used sparingly.

Objects are pointers

As we have seen in Querying objects, an object is a reference (a pointer), it contains values, and is an instance of a class.

Until recently, these pointers used to be 32 bits long, with one bit reserved to distinguish between immediate objects, such as SmallInteger Integer immediateSubclass: #SmallInteger instanceVariableNames: '' classVariableNames: '' package: 'Kernel-Numbers' integers, which are fully represented in the bits of the pointer, and regular objects, but with modern memory architectures, they are 64 bits.

For most purposes, we cannot directly access or manipulate object pointers from a running Smalltalk image, but there are a few exceptions to this rules. We can (1) iterate through memory using nextObject:, and (2) we can swap object pointers using become: and becomeForward:.

Walking through memory

You can obtain the first (non-immediate) object in memory by sending someObject to any class. You can then iterate through all objects by sending nextObject to each object until the value 0 is returned.

For example, this code will walk through memory and return a count of all the regular objects.

someBlElement := Object someObject.
currentElement := someBlElement nextObject.
count := 1.
[ currentElement = 0 ]
	whileFalse: [ 
		currentElement := currentElement nextObject.
		count := count + 1 ].
count.
  

Note that SystemNavigation Object subclass: #SystemNavigation instanceVariableNames: 'environment' classVariableNames: '' package: 'System-Support-Image' provides the utility methods SystemNavigation>>#allObjects allObjects "Answer an Array of all objects in the system. Fail if there isn't enough memory to instantiate the result." <primitive: 178> ^ self primitiveFailed and SystemNavigation>>#allObjectsDo: allObjectsDo: aBlock "Evaluate the argument, aBlock, for each object in the system, excluding immediates such as SmallInteger and Character." self allObjectsOrNil ifNotNil: [ :allObjects| allObjects do: aBlock ] ifNil: [ self error:'Error allocating a big enough array for all objects' ] , which are probably more convenient to use.

SystemNavigation default allObjects size.
  

In this case however the result includes immediate objects.

There also exist someInstance and nextInstance, which are specific to a given class. These are used to implement Context>>#allInstances allInstances "Answer all instances of the receiver." <primitive: 177> "The primitive can fail because memory is low. If so, fall back on the old enumeration code, which gives the system a chance to GC and/or grow. Because aBlock might change the class of inst (for example, using become:), it is essential to compute next before aBlock value: inst. Only count until thisContext since this context has been created only to compute the existing instances." | inst insts next | insts := WriteStream on: (Array new: 64). inst := self someInstance. [inst == thisContext or: [inst == nil]] whileFalse: [next := inst nextInstance. insts nextPut: inst. inst := next]. ^insts contents .

Dictionary allInstances.
  

Swapping and forwarding object pointers

In Smalltalk it is possible to swap the identities of two objects.

If you send foo become: bar, then every references to foo now points to bar, and vice versa. The main applications are (1) temporarily replace an object by a “wrapper” around that object (such as a logging proxy — see Overriding doesNotUnderstand:, or (2) to migrate objects in a running system from an old implementation to a new one.

Consider the following example:

becomePoints
	<gtExample>
	| pt1 pt2 pt3 |
	pt1 := 0 @ 0.
	pt2 := pt1.
	pt3 := 100 @ 100.
	pt1 become: pt3.
	self assert: pt1 equals: 100 @ 100.
	self assert: pt1 == pt2.
	self assert: pt3 equals: 0 @ 0.
	^ {pt1.
		pt2.
		pt3}
    

In this example, pt2 points to (is identical to) pt1. After swapping pt1 and pt3, pt2 still points to the same object as pt1 i.e., pt1 == pt2 —, but it is now a different object, namely 100@100. We can see this clearly by inspecting the result.

There is also becomeForward:, which does almost the same thing, but instead of swapping pointers, everything that pointed to the receiver now points to the argument, and pointers to the old object are lost. In this variant, pt1 and pt2 end up pointing to pt3, and the old 0@0 point is lost.

becomeForwardPoints
	<gtExample>
	| pt1 pt2 pt3 |
	pt1 := 0 @ 0.
	pt2 := pt1.
	pt3 := 100 @ 100.
	pt1 becomeForward: pt3.
	self assert: pt1 equals: 100 @ 100.
	self assert: pt1 == pt2.
	self assert: pt2 == pt3.
	^ {pt1.
		pt2.
		pt3}
    

Here's the result:

Changing the class of an object

It is also possible to change the class of an object, without swapping any pointers, using Object>>#primitiveChangeClassTo: primitiveChangeClassTo: anObject "Primitive. Change the class of the receiver into the class of the argument given that the format of the receiver matches the format of the argument's class. Fail if receiver or argument are SmallIntegers, or when the format of the receiver is different from the format of the argument's class, or when the arguments class is fixed and the receiver's size differs from the size that an instance of the argument's class should have. Note: The primitive will fail in most cases that you think might work. This is mostly because of differences in the format. As an example, '(Array new: 3) primitiveChangeClassTo: Morph basicNew' would fail for two of the reasons mentioned above. Array is variable and Morph is fixed (different format - failure #1). Morph is a fixed-field-only object and the array is too short (failure #2)." <primitive: 115> self isReadOnlyObject ifTrue: [ ^ self modificationForbiddenFor: #primitiveChangeClassTo: value: anObject ]. self primitiveFailed .

Caveat: while become: can be used to swap the pointers of any non-immediate objects (you can't swap an instance of Object with an instance SmallInteger), primitiveChangeClassTo: can only be used to change the class of an object to another class with the same format . For details, see the method comment of .

Here's an example where this works. We create anon as a new, anonymous subclass of Object ProtoObject subclass: #Object instanceVariableNames: '' classVariableNames: 'DependentsFields' package: 'Kernel-Objects' . We create anObject as an instance of Object, and change its class to anon. We then compile a new method in anon and verify that the object understands this new method.

anonymousClassInstance
	<gtExample>
	| anon anObject |
	anon := Object newAnonymousSubclass.
	anObject := Object new.
	anObject primitiveChangeClassTo: anon new.
	anon compile: 'thisIsATest ^ 2'.
	self assert: anObject thisIsATest equals: 2.
	self should: [ Object new thisIsATest ] 
		raise: MessageNotUnderstood.
	^ anObject
    

NB: the argument to primitiveChangeClassTo: is an object , not a class. The reciever's class is changed to be the same as that of the argument.

This technique should be used sparingly. In fact, there are very few usages in the system.

#primitiveChangeClassTo: gtSenders.
  

There is also Behavior>>#adoptInstance: adoptInstance: anInstance "Change the class of anInstance to me. Primitive (found in Cog and new VMs) follows the same rules as primitiveChangeClassTo:, but returns the class rather than the modified instance" <primitive: 160 error: ec> anInstance primitiveChangeClassTo: self basicNew. ^self , which returns the class instead of the instance, but it is also rarely used in practice.

#adoptInstance: gtSenders.