This looks cool, but the hard problem is migration, not loading. Consider the simple case:
I implement a function foo
foo calls bar.
I find a bug in foo and recompile a fixed version.
The linker updates the symbol for foo.
bar returns and continues running the old version of foo.
Systems that do this well do on-stack replacement and will patch the return point (instruction after the call) in the old foo to point to a trampoline that rewrites the stack frame as the new foo expects. This is really hard.
In C/C++, you can also take the address of a global or a function pointer and nothing in the system can tell the difference between that and an integer, so if you load a new version then some things may call the old version via function pointer (avoidable if you always go via PLTs) or may write to an old version of the global (avoidable if you always put globals in the same location, but this requires adding padding around them, which can break things that expect certain globals to be fixed-sized things in a segment).
That said, having the linker part of the tooling done in a language-agnostic way is a great starting point for anyone wanting to add this functionality in their compiler.
i ran into a similar issue (function pointers for callbacks getting stale after reload) for a hot-reloading renderer that I’ve been writing in C/C++, and addressed it via a “custom PLT” that has fixed-position forwarders to dynamic-positioned functions. I wrote a post about it if anyone’s interested in the implementation details…
This could be something that a specialised linker might want to do; I’ll be keenly following this project :)
Is that really something a linker like this would need to do though? Also, what if you patch foo to not call bar anymore at all? There will be no place to return to… Attempting to do something like this sounds just too hairy to be worth it. Naively, I would only expect new calls to foo to use the updated version and would be fine with that. This is also the typical behaviour you’d get in a REPL when replacing a function definition.
Is that really something a linker like this would need to do though?
A linker wouldn’t be able to do it, but it is necessary if you want the user model that you compile some new C/C++ code and it now exists in the running program.
Also, what if you patch foo to not call bar anymore at all? There will be no place to return to… Attempting to do something like this sounds just too hairy to be worth it.
Only if you’re unmapping the old objects. If you’re not, then the new objects and the old are both there. That’s a fairly common approach for fix-and-continue development.
Naively, I would only expect new calls to foo to use the updated version and would be fine with that. This is also the typical behaviour you’d get in a REPL when replacing a function definition.
Then you need to do on-stack replacement and you need to provide indirection for function pointers.
The article does mention your foo() calls bar() modify foo() scenario in a footnote:
My first pass would be to require the program to itself return control to the linker-loader so that the program’s image can be safely edited. This means the program would need some code to recognize that it has been requested to reload and return all the way up the stack on all threads so that its code can be modified.
Ouch. Well, I’m willing to accept a compromise where code can only be reloaded when it’s not on the stack, or even when the program is idle; that would still be super useful.
That’s still quite useful, but it requires some communication between the linker-loader and the program to know when it’s in a usefully quiesced state. In a Cocoa app, for example, you typically have a main NSRunLoop instance and so reloading code when you’re in the blocking state there would be fine (modulo any background threads, which would also have to rendezvous).
That still introduces problems if you modify any data structures. For example, if you add a field to a structure (or even a non-fragile-ABI Objective-C class) then you will have instances of the class lying around that don’t have that field and the offsets of others may change. When I wrote a Smalltalk compiler that targeted the Objective-C ABI, I solved this with some horrible hacks:
I loaded a complete new version of classes that had modified their layout.
I rewrote the class-table entry to point to the new class and duplicated all subclasses and updated their entries in the class tables to account for the new layout.
I indirected all accesses to the new ivars via a method.
I used the associated storage APIs to store these virtual ivars in instances of the old class.
This meant that new instances of the class had the new ivars, old instances still had the valid names and could pretend that they had the ivars, but cost more to access them. You still had to be a bit careful, because the new ivar would be uninitialised in the old class, whereas you might expect that your init-family methods had set it to something.
In normal Smalltalk systems, they typically integrate this with the GC and the same infrastructure required to implement become: (my become: implementation turned the object into an NSProxy subclass that forwarded everything to the target, which added some overhead, prevented direct ivar accesses from the outside, and worked only on objects with more than one pointer’s worth of ivar space, not counting isa).
Objective-C was easier than C here, because you can’t store Objective-C objects on the stack (well, not since Objective-C 4, the version before Objective-C 2.0, because Apple does counting a strange way).
Xcode used to have a “fix and continue” mode like this. IIRC it was pretty finicky. The feature vanished quite a while ago, probably in the transition from GCC to Clang.
This looks cool, but the hard problem is migration, not loading. Consider the simple case:
foo
foo
calls bar.foo
and recompile a fixed version.foo
.bar
returns and continues running the old version offoo
.Systems that do this well do on-stack replacement and will patch the return point (instruction after the call) in the old
foo
to point to a trampoline that rewrites the stack frame as the newfoo
expects. This is really hard.In C/C++, you can also take the address of a global or a function pointer and nothing in the system can tell the difference between that and an integer, so if you load a new version then some things may call the old version via function pointer (avoidable if you always go via PLTs) or may write to an old version of the global (avoidable if you always put globals in the same location, but this requires adding padding around them, which can break things that expect certain globals to be fixed-sized things in a segment).
That said, having the linker part of the tooling done in a language-agnostic way is a great starting point for anyone wanting to add this functionality in their compiler.
i ran into a similar issue (function pointers for callbacks getting stale after reload) for a hot-reloading renderer that I’ve been writing in C/C++, and addressed it via a “custom PLT” that has fixed-position forwarders to dynamic-positioned functions. I wrote a post about it if anyone’s interested in the implementation details…
This could be something that a specialised linker might want to do; I’ll be keenly following this project :)
Is that really something a linker like this would need to do though? Also, what if you patch
foo
to not callbar
anymore at all? There will be no place to return to… Attempting to do something like this sounds just too hairy to be worth it. Naively, I would only expect new calls tofoo
to use the updated version and would be fine with that. This is also the typical behaviour you’d get in a REPL when replacing a function definition.A linker wouldn’t be able to do it, but it is necessary if you want the user model that you compile some new C/C++ code and it now exists in the running program.
Only if you’re unmapping the old objects. If you’re not, then the new objects and the old are both there. That’s a fairly common approach for fix-and-continue development.
Then you need to do on-stack replacement and you need to provide indirection for function pointers.
The article does mention your
foo()
callsbar()
modifyfoo()
scenario in a footnote:Ouch. Well, I’m willing to accept a compromise where code can only be reloaded when it’s not on the stack, or even when the program is idle; that would still be super useful.
That’s still quite useful, but it requires some communication between the linker-loader and the program to know when it’s in a usefully quiesced state. In a Cocoa app, for example, you typically have a main
NSRunLoop
instance and so reloading code when you’re in the blocking state there would be fine (modulo any background threads, which would also have to rendezvous).That still introduces problems if you modify any data structures. For example, if you add a field to a structure (or even a non-fragile-ABI Objective-C class) then you will have instances of the class lying around that don’t have that field and the offsets of others may change. When I wrote a Smalltalk compiler that targeted the Objective-C ABI, I solved this with some horrible hacks:
This meant that new instances of the class had the new ivars, old instances still had the valid names and could pretend that they had the ivars, but cost more to access them. You still had to be a bit careful, because the new ivar would be uninitialised in the old class, whereas you might expect that your
init
-family methods had set it to something.In normal Smalltalk systems, they typically integrate this with the GC and the same infrastructure required to implement
become:
(mybecome:
implementation turned the object into anNSProxy
subclass that forwarded everything to the target, which added some overhead, prevented direct ivar accesses from the outside, and worked only on objects with more than one pointer’s worth of ivar space, not countingisa
).Objective-C was easier than C here, because you can’t store Objective-C objects on the stack (well, not since Objective-C 4, the version before Objective-C 2.0, because Apple does counting a strange way).
Xcode used to have a “fix and continue” mode like this. IIRC it was pretty finicky. The feature vanished quite a while ago, probably in the transition from GCC to Clang.