12
$\begingroup$

Consider an object-oriented language like Java. Typically, when a class is modified and recompiled, the software which uses that class must be closed and restarted, and the program state may need to be saved to disk in order to be restored.

However, it is theoretically possible that an implementation could allow "hot reloading" of classes at runtime. This would be beneficial for availability in long-running software such as a networked servers, and also potentially useful for interactive debugging ─ you would not have to restart the program to try out changes.

To be clear, the idea here is not just that new instances created after the update will use the new class. The idea is that when the class is updated, existing instances of the old class are migrated to become instances of the new class, including any changes required to their memory layouts. For example, new fields added to existing objects could be initialised with some default value, or perhaps the language could have a mechanism allowing the user to control how existing objects are re-initialised.

How could hot reloading of classes work in practice, and what would its limitations be?

$\endgroup$
13
  • $\begingroup$ Dart is one language I know that supports this $\endgroup$ Commented May 24, 2023 at 17:00
  • $\begingroup$ @mousetail I'd definitely be interested in an answer about how Dart handles this. $\endgroup$ Commented May 24, 2023 at 17:01
  • $\begingroup$ Java allows dynamic loading of classes at runtime; i use that in the unit tests for my Vyxal to JVM compiler $\endgroup$ Commented May 24, 2023 at 17:04
  • $\begingroup$ @Seggan Yes, but as far as I know, existing objects would still be associated with the old version of the class. Perhaps there is some way to do it in Java by replacing the class pointer in the object's header? If there is an example of this being done in practice (perhaps using sun.misc.unsafe?) it would make for a good answer. $\endgroup$ Commented May 24, 2023 at 17:07
  • 2
    $\begingroup$ Objective-C lets you mess with the dtable at runtime, and it works because literally every method call is dynamic. This is a surprisingly well-documented thing, though I’ve only seen it used for DI. $\endgroup$ Commented May 24, 2023 at 18:53

4 Answers 4

10
+50
$\begingroup$

The Objective-C Runtime

Objective-C has surprisingly strong runtime reflection capabilities, on par with that of JavaScript. Here's a couple ways you could do it with the Objective-C runtime library.

Option 1: Register and deregister classes

Objective-C lets you create new classes at runtime with objc_allocateClassPair, and deregister existing classes with objc_disposeClassPair. Whenever a class needs to be reloaded, you could register the new version as a new class and dispose the existing one. The problem with this approach is, as per the documentation for objc_disposeClassPair:

Do not call this function if instances of the cls class or any subclass exist.

However, you can change the class of an object at runtime with object_setClass. Therefore, you could still use this approach with the following process:

  1. Allocate, configure, and register the new version of the class.
  2. Iterate over all instances of the old version of the class, and change them to the new version. Objective-C doesn't have an "all instances" function in its runtime, so this would have to be a static property on the class.
  3. Dispose the old version of the class.

Option 2: Mess with the class metadata

This allows you to work without having to worry about subclasses or iterating over existing instances, but may be much more involved. You'd have to check each part of the definition individually.

Methods

To change the implementation of an existing method, use method_setImplementation or method_exchangeImplementations. This is a well-supported pattern known as method swizzling; as I mentioned in a comment, this is usually used for dependency injection.

To add new methods, use class_addMethod.

Removing methods is actually not something that's not supported! If you're making a new language, you could probably add support for this in your language, but Objective-C doesn't have it. However, there is a way for an object to pretend like it doesn't have a method, even at compile time, so we can use the same technique here.

The first step is to write the method implementation as follows:

- (void)exampleMethod {
  [self doesNotRecognizeSelector:_cmd];
}

doesNotRecognizeSelector: is the method invoked by the runtime when all attempts to find the implementation of a method have failed. _cmd is an implicit argument that all methods take, which is the method's selector (a representation of the method name).

The second step is to make respondsToSelector: and instancesRespondToSelector: also deny it:

- (BOOL)respondsToSelector:(SEL)aSelector {
  if (sel_isEqual(@selector(exampleMethod), aSelector)
    return NO;
  return [super respondsToSelector:aSelector];
}

+ (BOOL)instancesRespondToSelector:(SEL)aSelector {
  if (sel_isEqual(@selector(exampleMethod), aSelector)
    return NO;
  return class_respondsToSelector(self, aSelector);
}

Then, you can drop these in with the functions mentioned above.

Properties

Properties are the public interface used to access instance members, while ivars are the underlying storage for them. Changing properties is fairly straightforward: class_replaceProperty to change an existing one and class_addProperty to add a new one, while deleting old ones is basically in the same boat as deleting methods. Each property corresponds to a pair of selectors (e.g. foo for the getter and setFoo: for the setter).

Ivars

This is where things get interesting. Messing with ivars means changing the actual memory layout of the class. Surprisingly, this is just supported: class_setIvarLayout for owned ivars and class_setWeakIvarLayout for weak ones. You may have to take care that existing instances don't become dangerous when you do this.

Protocols

Protocols are Objective-C's equivalent of Java's interfaces. Once again, adding new ones is supported but removing existing ones is complicated. Adding a new one is just a call to class_addProtocol plus implementing the necessary methods.

You can't remove the protocol from the class list, but you can make the class act like it's not there by overriding conformsToProtocol:

+ (BOOL)conformsToProtocol:(Protocol *)protocol {
  if ([protocol isEqual:@protocol(ProtocolToRemove)])
    return NO;
  return class_conformsToProtocol(self, protocol);
}
$\endgroup$
2
$\begingroup$

This is a feature of the language Erlang and the library functions known as OTP. The combination allows classes written following certain guidelines to be upgraded on the fly. During the upgrade process existing calls to the classes use the "old" code, while new invocations use the "current" code. Over time all old calls run to the return point and the system will be able to accept further upgrades. It is a bit of convoluted reading, but you may start here Erlang/OTP documentation

I will try to describe the most important ways a class has to be adapted to handle this update. But first, it is not really for the rapid software debug process but mainly for the deployment of software updates to a distributed system with many processors and extremely high demands on availability. Erlang/OTP was developed for telephone exchanges that preferrably should run for many years between reboots.

The support is part of the OTP "framework" as only classes deriving from this framework are supported. A class has a specific procedure that is called when a new version is installed. This procedure will do any changes needed. In addition a number of tools on the developement system creates "update" files that describe the update. These are copied to the target system and the update is initiated.

The Erlang runtime knows about the difference between "old" and "current" code as well. Note that Erlang is compiled into a type of byte-code that is interpreted on the target system (similar to how java originally worked).

$\endgroup$
0
1
$\begingroup$

I made some experiments, about recompiling classes at runtime, within Ike (that is my pet OO programming language, built on top of Lua).

There are two classes of implementations of OO langs: those where recompiling... just does nothing, and the ones where the action is supported. Just for an example: when you recompile a class in Python, nothing happens. The object already instanciated continue to use the old definition. Only new objects will inherit the new behaviors. Ruby is different, because Ruby had been designed with the same concepts of late-binding borrowed from Smalltalk. When you recompile dynamically a class (in Ruby) the new behaviors are immediately effective even for the already instantiated objects. The same thing happens in Smalltalk.

Well: if you want this feature you must code it. The first behavior (the one of Python, for example) is the simplest one. It is a byproduct of the simple design of the system. Instead, if you want the second one (the immediate propagation of a new behavior) you have to face a more complex design.

As a first thing: after recompiled the class you must be able to cycle all the existing objects that directly inherit from that class and send them the "become" message. That is: you must inform them about the new class which will be their reference from now. Obviously this should take place in a protected time-segment. For many simple classes there are not problems, but the recompilation of some particular system classes may corrupt the integrity of the whole system (if the replacement does not take place correctly).

A problem you may consider secondary is this: what do you want to do with the old class? When you recompile a class you are not "replacing" it. You are just adding another object to the system. If the old class is not completely removed, you risk to have a system that is a timer-bomb. The old class MUST be removed. But you can not remove it until the last instance of that class had been updated.

Another not obvious (at least for me) problem is about the source. Many debugging features use to connect bad behaviors or exceptions occurred to a line within the source which defined the objects involved. When you recompile dynamically the references to the new source should replace the original ones. You can not point user to a line of source witch is not a real source...

The problem here is that the user (the one responsible of the recompilation) is probably working within a big file of source code, the original file from which the system had been initialized. But she is changing just one method of a given class, or maybe a whole class. She does not recompile the whole module, but just a piece.

In any case: the references the VM knows about the new object recompiled will be no connected to the source file, but to a string that feed the compiler. All next signals of errors occurred within that class will have that string, and not the original file, as reference. Hence you have to implement a "reconciliation" strategy to give user useful feedbacks, in case of exceptions.

...this is my cent to the whole question.

$\endgroup$
-3
$\begingroup$

Isn't this a regular feature of Java? As it's running "bytecode" with its "VM"/interpreter, Java offers reflection as an interpreter command can return the class metadata associated with the objects it's currently handling, including allowing to manipulate these during runtime. JIT (just-in-time compilation) can be a thing, Markus 'Notch' Persson using some hotloading while developing original Minecraft.

I'm not familiar with Java's particular features of this kind, I also don't know if there's migration strategies for existing objects vs. existing ones, but I do think the Java VM as well as the Java language front-end are a popular example for supporting such operations.

$\endgroup$
5
  • $\begingroup$ No, this simply isn't a feature of Java. Java reflection does not allow any way to change the class of an object which already exists. $\endgroup$ Commented Jun 15, 2024 at 0:13
  • $\begingroup$ Reflection implies looking from the outside at the metadata about objects and classes. But means 1. there is such metadata internally collected, 2. reflection exposes it to the programmer. A step 3. can be, based on the metadata being available (vs. not), consttructs can be provided to also change things at runtime. Indeed, reflection alone is not sufficient for hotloading. My point was that Java VM afaik can do some, Java lang maybe too (not sure). Same mechanisms that power reflection are a precondition, otherwise is harder to change things at what memory addresses happen to be. $\endgroup$ Commented Jun 15, 2024 at 0:31
  • 1
    $\begingroup$ Hot reloading doesn't necessarily require a runtime mechanism for inspecting type members, for the same reason that database migrations don't need to inspect table columns at runtime. If the compiler has access to both versions of the class (or database table), it can generate code to update values (or rows) of that class (or table). As for Java, this definitely isn't a feature of the language, but if there is a VM which supports it then you should cite it ─ it's not useful to just say "maybe there is one", because the question is how it works in practice and what its limitations are. $\endgroup$ Commented Jun 15, 2024 at 0:39
  • $\begingroup$ I'm not really familiar with this, but the original question even less so (assumes this isn't a thing with Java). So OK: the typical way for a user would be to implement/derive-from ClassLoader to hot-reload the class instead of re-using the first loaded definition, then set it for the current thread. Loading a precompiled *.class source or otherwise compiling, but you relaxed that. May implement some object migration/copying even? Similar for other langs. Based on an interpreter/VM performing a lookup/dispatch for class names, method names, supply it code/bytecode to pass into execution. $\endgroup$ Commented Jun 15, 2024 at 2:02
  • 3
    $\begingroup$ "I'm not really familiar with this" - in this case, it isn't necessary to post an answer. You've posted a lot of answers in short succession and a few of those, including this one, disclaim relevant knowledge of the topic. Those are unlikely to be strong answers, and it would probably be better to look for questions you know about and are able to answer. You might also like to go a little slower and build up familiarity with how things work well here. $\endgroup$ Commented Jun 15, 2024 at 4:29

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.