Rendered at 06:50:13 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
inigyou 15 hours ago [-]
Self-modifying code is cool. It's a shame we had to disable it for security.
mcculley 14 hours ago [-]
Not just security. Instruction caches must also be aware of self-modifying code.
WalterBright 13 hours ago [-]
Back in the 1980s, text editors had configuration files. The configuration file would be read every time the editor was loaded. This was very slow on a floppy disk system.
I realized that, instead of a configuration file, I could configure the executable instead! So, any changes in configuration meant the editor would patch its own exe file!
This marvelous technique came to an end when attempts to stop malware got folded into the operating system.
weinzierl 13 hours ago [-]
Some early systems, like TeX and I believe some Lisps, took this to the extreme. Instead of patching, they loaded their config once and then dumped the configured process image to a file, which was used in subsequent invocations.
drfuchs 5 hours ago [-]
TeX was developed on DECsystem-10/20 machines (36-bit words, your choice of byte size). The various operating systems (Tops20, Sail/Waits, ITS) were superior to Unix in a few ways, one of which was that when a process was suspended (think control-Z, or even control-C) you could issue the built-in shell SAVE command that would save the entire state of the suspended process into a new executable, data segment as well as code. So, on these systems, you could run TeX, have it load a bunch of macros and such, and then SAVE the result as a real, pre-configured executable for the world to run. Easy-peasy.
This was true for the original TeX78 written in Sail, as well as the ultimate TeX82 written in Knuth's WEB macro language on top of Pascal (that nowadays typically gets transpiled to C). Other programs did similar stuff; the feature was in the OS way before TeX started.
(Gory detail: Actually, TeX went a little further, to free up all possible address space for the final executable: The version that could initialize various hash tables and hyphenation trie tables could dump (nee serialize) a binary file of the resulting data structures; then a slimmed-down version that didn't have that code would read the binary info back in to recreate the initial data structure state, and that's what you'd SAVE the production executable from.)
For the unix-y versions of TeX, there was effort made to mimic this sort of thing in user-land with "undump", but admirable as it was, it was a hack, I'm told. These days, everything is so fast, it's not clear that this feature would be worth it anyway.
Source: me; I was there.
inigyou 4 hours ago [-]
I don't think undump is a hack except to the extent the whole thing is a hack. Linux shoves a lot of stuff to userspace that other operating systems put in kernel space; this is just another one of them.
The program loader is not magic - it just reads a list of things to mmap and then mmaps them. If you write a thing that writes a list of what's mmapped and call it the unloader... fine?
BillStrong 9 hours ago [-]
Emacs does this to create its image with all the added functionality above the minimum required to run elisp, then has a mechanism to pull in text files because elisp is just text anyway.
They just chucked the old system for a portable version of it, but until this last release, they still had to option of doing it the old school way.
amszmidt 11 hours ago [-]
Don’t think TeX ever did that, that was a “simple” Pascal program.
Lisp Machines though.. updating the operating system was by loading bunch of compiled files that replaced currently loaded functions in memory.
Then again, Lisp Machines where very proud of self modification — the CADR had a fun feature where it could modify the next instruction depending on things…
How the world has changed.
weinzierl 9 hours ago [-]
Pascal TeX to the best of my knowledge never did it but the web2c version does.
The description is a bit confusing because it can both dump only the warmed up interpreter image or the whole process, I think.
"With the program undump, you can use `core' to reconstitute a preloaded executable, which does not need to read a `.fmt' file to get started. Although preloaded executables save startup time, they have a big disadvantage: neither the disk space to store them nor their code segments (at runtime) can be shared. Therefore, if both tex and latex are running, twice as much memory will be consumed, to the general detriment of performance."
> Don’t think TeX ever did that, that was a “simple” Pascal program.
Not exactly the entire process image, no, but essentially all of its data in a single chunk. It’s a peculiar Pascal program because it bypasses basically all of Pascal’s typing, records, etc., and instead builds its own from a set of (WEB) macros on top of a giant untyped array. Quoth Knuth in TeX: The Program §115:
> The dynamic storage requirements of TeX are handled by providing a large array mem in which consecutive blocks of words are used as nodes by the TeX routines. Pointer variables are indices into this array [...].
There are a few more areas designated for specific purposes, but at the end of the day (§1302) it works out about the way you’d expect:
procedure store_fmt_file;
[...] begin ⟨ If dumping is not allowed, abort 1304 ⟩
⟨ Create the format ident, open the format file, and inform the user that dumping has begun 1328 ⟩;
⟨ Dump constants for consistency check 1307 ⟩;
⟨ Dump the string pool 1309 ⟩;
⟨ Dump the dynamic memory 1311 ⟩;
⟨ Dump the table of equivalents 1313 ⟩;
⟨ Dump the font information 1320 ⟩;
⟨ Dump the hyphenation tables 1324 ⟩;
⟨ Dump a couple more things and the closing check word 1326 ⟩;
⟨ Close the format file 1329 ⟩;
end;
WalterBright 12 hours ago [-]
On a floppy system, it's much faster to just patch it on disk than rewrite it!
And didn't stop doing this until 2020! (Specifically Emacs 27.1, which replaced "unexec", which dumped the process image, with the "portable dumper", which despite the name does not.)
BillStrong 9 hours ago [-]
And you could still use the unexec method until this very latest release a few days ago, if you didn't want the pdumper.
UltraSane 20 minutes ago [-]
When I was learning Python I wrote a program that stored data in its own .py file. I felt pretty smart.
badsectoracula 13 hours ago [-]
IIRC early Turbo Pascal versions worked like that too, there was some "setup" program that let you configure colors, etc, by modifying the COM/EXE file itself.
stevekemp 2 hours ago [-]
I still run Borland's Turbo Pascal on CP/M systems, and many programs had setup/configuration programs which would rewrite binaries for specific input/output devices.
Choosing between ADM-3A or ANSI terminals by running "WINSTALL" would rewrite the main Wordstart executable WS.COM appropriately for example.
akoboldfrying 4 hours ago [-]
Was the advantage that the exe file's sectors were likely to be contiguous on disk, so the config data could likely be read in a single pass through sectors on the same track, as compared to probably having to wait for a full revolution and a track seek for the separate config file approach? Or was it that the config file was verbose but compressed to a much smaller image in memory, thus fewer bytes to read?
inigyou 13 hours ago [-]
yes but it's easy enough to issue a cache flush when you modify the code.
The overhead of cache flushing means some old school techniques are no longer viable, like modifying a constant in the next instruction. However it is still interesting to write machine code snippets once and execute them many times, like the nested function trampolines. I had a case where I had RGB masks like R=0x00ff0000 etc (loaded at startup once) and wanted to convert 0x00rrggbb to match the mask (so no-op in the common case but not always) which could have involved setting the shift amounts in a series of shift instructions.
The Linux kernel uses self-modification to change branches depending on whether certain features are on. For example when a user-mode process starts tracing a certain function, it adds code to the beginning of that function to trace the call, otherwise it pads that space with a no-op. JIT compilers also make good use of knowing whether a class has any subclasses, which is statically unknowable in Java but dynamically knowable.
jasomill 3 hours ago [-]
The Microsoft Detours[1] library does this for arbitrary Windows API calls. I've used it production to fix simple bugs in third-party software no longer supported by vendors.
For example, I have a program that opens Adobe Acrobat Reader as an out-of-process COM server, but tends to leave phantom Acrobat processes hanging around after it quits. To fix this, I wrap CreateProcess in a function that adds any Acrobat processes created to a job object[2] set up to make Windows automatically kill them when the application closes.
Yep. Even outside of tracing, there are several different ways that the Linux kernel patches itself:
- Static calls: like a call to a global function pointer, except instead of loading a function pointer and doing an indirect call, the code is patched to do a direct call to the destination
- Static keys: like an if statement testing a global boolean, except instead of loading a boolean and doing a conditional branch, the code is patched to do either an unconditional branch or a nop
- Runtime constants: like a load of a global variable, except instead of loading, the value is patched directly into the code
- Alternatives: selects one of multiple possible instruction sequences depending on (usually) whether the CPU supports specific instructions
It's really fascinating to see the kind of fun efficient stuff you can do when you have that level of low-level control. Not just code patching but things like RCU as well.
> However it is still interesting to write machine code snippets once and execute them many times, like the nested function trampolines.
I slightly disagree on this though. In my experience writing code with Clang blocks (which don't use trampolines), they're often useful for code organization even if the callback will only be called once. Therefore, even ignoring security issues, I think GCC choosing a design that required cache flushing was a mistake - certainly in retrospect (as cache flushing has become more expensive over the years), but perhaps even at the time. I did some research, and trampolines were introduced in GCC 2.0, which already included mprotect calls and/or cache flushes on some of the architectures it supported, such as MIPS. However, this was a relatively new development, and on most of the supported architectures it didn't do either of those things. But on MIPS it would do an mprotect every single time a trampoline was created, which can't have been fast.
inigyou 9 hours ago [-]
It's not the cache flush that's expensive, it's loading the new instructions that aren't cached. If you are flushing the very next instruction and then immediately executing it, that's expensive because of the serial dependency, but if you're generating new code, flushing it shouldn't be more expensive than if you were simply accessing new code for the first time. But on old systems you could modify the very next instruction with no penalty because there wasn't a cache. You can still do that and probably faster than those old systems could (they were slower because of not having a cache, everything was an uncached access), it's just a waste of most of the new system's performance.
BTW you can do all of this cool stuff in user mode on Linux too (but not on OpenBSD) - you just have to opt in to executable stack and/or writable .text. I could have written the dynamic shift instruction generator I mentioned, but I didn't want to spend the effort, but I imagined having a language with actual support for something like that (like static keys for variables).
codedokode 12 hours ago [-]
Modifying a constant in code might make sense because it saves several precious bytes of variable storage.
inigyou 11 hours ago [-]
No, it might make sense because it saves precious memory bandwidth as well as uop count. This code wasn't called that often, generally once per drawing operation, but I don't like being uselessly wasteful - that mindset is how software got so bloated.
Your program is either front-end stalled by uop count or instruction cache latency, or back-end bound by memory bandwidth or ALU throughput or a serial dependency chain. It doesn't matter which is the bottleneck for this case, because inlining constants improves most of the above!
inigyou 9 hours ago [-]
s/stalled/bound/
When the back end is the bottleneck, the front end stalls and vice versa, though I'm not sure how all processors report it.
anitil 6 hours ago [-]
I was re-reading Ian Lance Taylor's series of articles on Linkers [0], and one thing I didn't realise is that using dynamically linked libraries almost requires self-modifying code unless you want to resolve all function calls are start-time (which would make startup slower).
I'm still working my way through it so it's possible that I've misunderstood this section, though, and one question I haven't answered is how they get around the typical restriction on w+x pages.
It was maybe cool 50 years ago or so. Nowadays it's no longer needed. Possible performance gains of such code are marginal and modern programming languages allow generating many specialized and optimized code pieces using the same template, so that self-modification is no longer needed.
There is also JIT (like in regexp engines), but it's different story.
fweimer 11 hours ago [-]
It's still beneficial on some x86-64 implementations to rewrite indirect jumps (as used in PLT stubs) to direct jumps when feasible. For example, AMD says this about the Zen 4 architecture:
> Only a limited number of indirect targets that cross a 64MB aligned boundary relative to the branch address can be tracked in the indirect target predictor. Software should limit the number of indirect branch targets that cross such a boundary.
And one way doing this is to replace the indirect branch with a direct branch, which supports a 32-bit signed displacement.
inigyou 9 hours ago [-]
It's pretty much always beneficial to do something more directly. Doing less work is always better than doing more work. The slowness of modern software is the result of a stack of abstractions acting like a stack of interpreters. You write something in React, that manipulates a React object tree and shadows it to a DOM, which gets shadowed to an internal object tree which gets laid out and shadowed to a stack of GPU layers which gets written out as drawing commands... When you want to scroll up there's so much work to do. To make it fast, cut through layers and minimize work. In the 1990s, scrolling up meant calculating how many pixels to scroll, blitting that many pixels in the main framebuffer (usually GPU accelerated) and then rendering the new pixels at the bottom. There wasn't even a double buffer. Very little abstraction there, just the shortest path to achieve the desired result. The GPU driver did abstract the blit and rendering operations, of course, and the mouse driver abstracted the scrollwheel event, and you may have a wrapper component in your GUI tree that manages a viewport over a larger virtual component, but it's all kept as direct as practical. I can't imagine any electron app using the blit-pixels-up approach.
I remember once learning of a runtime environment that would inline class functions. For example they wrote an OS, and if you had an object of a SATA hard drive class, it would copy the function code and inline the drive ID. I don't remember how well it worked for them.
A related idea is the "tracing JIT". You know how you expect a JIT to translate one function at a time? A tracing JIT doesn't - it follows the program logic wherever it goes, through whatever control flow, and compiles all of it until it decides to stop. The most well known implementation is probably LuaJIT.
BobbyTables2 8 hours ago [-]
Have always wondered, why can’t nested functions just be normal functions inside a namespace?
It seems silly to expose a tiny helper to the entire compilation unit when it is only meant to be used inside one function…
wavemode 6 hours ago [-]
> why can’t nested functions just be normal functions inside a namespace
Because GCC's nested functions are closures - they can access local variables within the function.
At first, I was annoyed by having to read AT&T syntax. Then I was disoriented by realizing the next snippet was in AT&T syntax without the '%' sigil for registers. But the technique is cool.
uecker 15 hours ago [-]
Thanks. Sigils fixed (may take a couple of minutes).
Dwedit 15 hours ago [-]
What do you need the executable stack for? You call using a function pointer, there's no executable read/write memory involved in using a function pointer.
jcranmer 12 hours ago [-]
A nested function requires an extra parameter for the nested stack pointer, which is passed in a dedicated register on most ABIs. You can't spell this kind of function type in C. To make a C-callable function pointer, the compiler needs to generate a little bit of code (a trampoline) that stuffs the appropriate stack pointer in the appropriate register.
That trampoline needs to live somewhere. Since the function is inherently noncallable after the stack returns, and C programmers hate it when their compiler sneaks in extra malloc calls under the hood, the compiler decides to stick the trampoline on the stack instead of heap-allocating it.
tom_ 15 hours ago [-]
Nested functions may require a context pointer of some kind, which the caller can't supply. One way of doing this: create a thunk on the stack that provides the context pointer, and use that address as the pointer to the nested function.
But now the stack needs to be executable.
ok123456 13 hours ago [-]
Doesn't x86 actually support nested call pointers using enter to natively support this in Pascal?
klodolph 13 hours ago [-]
It’s not a question of ISA support. If you call via a function pointer, how do you supply the pointer to the data? (It has to either be in a separate place from the function code, or the same place. One requires an ABI change, the other an executable, writable section of memory.)
WalterBright 13 hours ago [-]
> If you call via a function pointer, how do you supply the pointer to the data?
D has the notion of a "delegate", which is a (function pointer) and (context pointer) pair. This is incredibly useful, because delegates can:
1. call nested functions that need a pointer to the stack frame of the nestee function
2. call member functions that need `this` pointer
3. call lambdas
4. call COM member functions
The neato thing about this is the ABI for delegates is all the same, so a function that gets a delegate parameter will work with any of 1..4. It's one of the most used features of D.
klodolph 13 hours ago [-]
Yes, that’s the “ABI” alternative that I was referring to.
uecker 12 hours ago [-]
This is why I want to have such a feature in C. It would be extremely useful for language interoperability.
This feature would IMO violate the contract that C allows you to specify memory layout of objects, to some level of detail (I am being a little vague about the “level of detail”).
Supporting closures, more or less, requires design decisions that are equivalent to choosing a specific layout for objects in an object-oriented language. C, as it is, makes none of these assumptions and you can translate a lot of different language ABIs into some C code (that may be clumsy). Keeping the abstraction that function pointers = pointers to entry points for functions, well, that’s frustrating for C programmers writing C programs, but extremely useful for interoperability.
uecker 9 hours ago [-]
At the moment we can not call nested functions or other things from other language from C, which is a pretty big hole in our interoperability story. I do not see why we need to make any design decision to specify object layout, we simply need a code pointer and static chain pair, which would then be sufficient to call arbitrary entities from other languages.
klodolph 3 hours ago [-]
> I do not see why we need to make any design decision to specify object layout
> we simply need a code pointer and static chain pair
The code pointer and chain pair needs an object layout. You would have to pick a specific layout, and it would not be compatible with other languages that have a different layout.
Right now I can do this:
struct a {
void (*fun)(void *ctx);
int data1;
int data2;
};
struct b {
void (*fun)(void *ctx);
void *ctx;
};
And I could call them:
struct a *aptr;
a->fun(aptr);
struct b *bptr;
b->fun(bptr->ctx);
There is only one neutral, maximally compatible option here—which is to have the function pointer separate from the arguments you want to pass in, and pass them in explicitly.
If you add closures to C you are making compatibility worse, not better.
uecker 19 minutes ago [-]
Except doing this manually does not allow me to directly call a C++ lambda, a Go closure, an Ada closure etc which I can not even express in C. So compatibility can not become worse, it is already maximally bad. And even where you can build a compatible solution in C, there are now different choices. Your examples already directly shows this contradicting your claim that here is only one option.
Adding such a type as a vocabulary type would fix all this. You can argue that we fix an object layout for a pointer pair, but this seems an acceptable trade-off to me. This seems far from your previous claim that this "requires design decisions that are equivalent to choosing a specific layout for objects in an object-oriented language." Note also that such a type can always adapt to different calling conventions of other languages by using the address of a static thunk as code pointer so it is very generic.
QuadmasterXLII 12 hours ago [-]
but that can just be executable heap?
klodolph 12 hours ago [-]
Yes, but the advice was W^X. Either a page of memory is executable or writable but not both at the same time.
mwkaufma 12 hours ago [-]
need to represent the "captured-variables"/closure
Dwedit 2 hours ago [-]
C# does closures by creating a class. When you run the function, captured local variables actually live inside the object instead of in the stack. This makes use of an object, but no memory pages need to become read/write/executable to make C#-style closures happen. You just have a combination function/object pointer (a delegate) instead of a single function pointer.
I realized that, instead of a configuration file, I could configure the executable instead! So, any changes in configuration meant the editor would patch its own exe file!
This marvelous technique came to an end when attempts to stop malware got folded into the operating system.
This was true for the original TeX78 written in Sail, as well as the ultimate TeX82 written in Knuth's WEB macro language on top of Pascal (that nowadays typically gets transpiled to C). Other programs did similar stuff; the feature was in the OS way before TeX started.
(Gory detail: Actually, TeX went a little further, to free up all possible address space for the final executable: The version that could initialize various hash tables and hyphenation trie tables could dump (nee serialize) a binary file of the resulting data structures; then a slimmed-down version that didn't have that code would read the binary info back in to recreate the initial data structure state, and that's what you'd SAVE the production executable from.)
For the unix-y versions of TeX, there was effort made to mimic this sort of thing in user-land with "undump", but admirable as it was, it was a hack, I'm told. These days, everything is so fast, it's not clear that this feature would be worth it anyway.
Source: me; I was there.
The program loader is not magic - it just reads a list of things to mmap and then mmaps them. If you write a thing that writes a list of what's mmapped and call it the unloader... fine?
They just chucked the old system for a portable version of it, but until this last release, they still had to option of doing it the old school way.
Lisp Machines though.. updating the operating system was by loading bunch of compiled files that replaced currently loaded functions in memory.
Then again, Lisp Machines where very proud of self modification — the CADR had a fun feature where it could modify the next instruction depending on things…
How the world has changed.
The description is a bit confusing because it can both dump only the warmed up interpreter image or the whole process, I think.
"With the program undump, you can use `core' to reconstitute a preloaded executable, which does not need to read a `.fmt' file to get started. Although preloaded executables save startup time, they have a big disadvantage: neither the disk space to store them nor their code segments (at runtime) can be shared. Therefore, if both tex and latex are running, twice as much memory will be consumed, to the general detriment of performance."
https://mirror.gutenberg-asso.fr/tex.loria.fr/texlive-htmldo...
Not exactly the entire process image, no, but essentially all of its data in a single chunk. It’s a peculiar Pascal program because it bypasses basically all of Pascal’s typing, records, etc., and instead builds its own from a set of (WEB) macros on top of a giant untyped array. Quoth Knuth in TeX: The Program §115:
> The dynamic storage requirements of TeX are handled by providing a large array mem in which consecutive blocks of words are used as nodes by the TeX routines. Pointer variables are indices into this array [...].
There are a few more areas designated for specific purposes, but at the end of the day (§1302) it works out about the way you’d expect:
[1] https://pharo.org/
Choosing between ADM-3A or ANSI terminals by running "WINSTALL" would rewrite the main Wordstart executable WS.COM appropriately for example.
The overhead of cache flushing means some old school techniques are no longer viable, like modifying a constant in the next instruction. However it is still interesting to write machine code snippets once and execute them many times, like the nested function trampolines. I had a case where I had RGB masks like R=0x00ff0000 etc (loaded at startup once) and wanted to convert 0x00rrggbb to match the mask (so no-op in the common case but not always) which could have involved setting the shift amounts in a series of shift instructions.
The Linux kernel uses self-modification to change branches depending on whether certain features are on. For example when a user-mode process starts tracing a certain function, it adds code to the beginning of that function to trace the call, otherwise it pads that space with a no-op. JIT compilers also make good use of knowing whether a class has any subclasses, which is statically unknowable in Java but dynamically knowable.
For example, I have a program that opens Adobe Acrobat Reader as an out-of-process COM server, but tends to leave phantom Acrobat processes hanging around after it quits. To fix this, I wrap CreateProcess in a function that adds any Acrobat processes created to a job object[2] set up to make Windows automatically kill them when the application closes.
[1] https://github.com/microsoft/detours
[2] https://learn.microsoft.com/en-us/windows/win32/procthread/j...
- Static calls: like a call to a global function pointer, except instead of loading a function pointer and doing an indirect call, the code is patched to do a direct call to the destination
- Static keys: like an if statement testing a global boolean, except instead of loading a boolean and doing a conditional branch, the code is patched to do either an unconditional branch or a nop
- Runtime constants: like a load of a global variable, except instead of loading, the value is patched directly into the code
- Alternatives: selects one of multiple possible instruction sequences depending on (usually) whether the CPU supports specific instructions
It's really fascinating to see the kind of fun efficient stuff you can do when you have that level of low-level control. Not just code patching but things like RCU as well.
> However it is still interesting to write machine code snippets once and execute them many times, like the nested function trampolines.
I slightly disagree on this though. In my experience writing code with Clang blocks (which don't use trampolines), they're often useful for code organization even if the callback will only be called once. Therefore, even ignoring security issues, I think GCC choosing a design that required cache flushing was a mistake - certainly in retrospect (as cache flushing has become more expensive over the years), but perhaps even at the time. I did some research, and trampolines were introduced in GCC 2.0, which already included mprotect calls and/or cache flushes on some of the architectures it supported, such as MIPS. However, this was a relatively new development, and on most of the supported architectures it didn't do either of those things. But on MIPS it would do an mprotect every single time a trampoline was created, which can't have been fast.
BTW you can do all of this cool stuff in user mode on Linux too (but not on OpenBSD) - you just have to opt in to executable stack and/or writable .text. I could have written the dynamic shift instruction generator I mentioned, but I didn't want to spend the effort, but I imagined having a language with actual support for something like that (like static keys for variables).
Your program is either front-end stalled by uop count or instruction cache latency, or back-end bound by memory bandwidth or ALU throughput or a serial dependency chain. It doesn't matter which is the bottleneck for this case, because inlining constants improves most of the above!
When the back end is the bottleneck, the front end stalls and vice versa, though I'm not sure how all processors report it.
I'm still working my way through it so it's possible that I've misunderstood this section, though, and one question I haven't answered is how they get around the typical restriction on w+x pages.
[0] Particularly this one https://www.airs.com/blog/archives/41
There is also JIT (like in regexp engines), but it's different story.
> Only a limited number of indirect targets that cross a 64MB aligned boundary relative to the branch address can be tracked in the indirect target predictor. Software should limit the number of indirect branch targets that cross such a boundary.
And one way doing this is to replace the indirect branch with a direct branch, which supports a 32-bit signed displacement.
I remember once learning of a runtime environment that would inline class functions. For example they wrote an OS, and if you had an object of a SATA hard drive class, it would copy the function code and inline the drive ID. I don't remember how well it worked for them.
A related idea is the "tracing JIT". You know how you expect a JIT to translate one function at a time? A tracing JIT doesn't - it follows the program logic wherever it goes, through whatever control flow, and compiles all of it until it decides to stop. The most well known implementation is probably LuaJIT.
It seems silly to expose a tiny helper to the entire compilation unit when it is only meant to be used inside one function…
Because GCC's nested functions are closures - they can access local variables within the function.
That trampoline needs to live somewhere. Since the function is inherently noncallable after the stack returns, and C programmers hate it when their compiler sneaks in extra malloc calls under the hood, the compiler decides to stick the trampoline on the stack instead of heap-allocating it.
But now the stack needs to be executable.
D has the notion of a "delegate", which is a (function pointer) and (context pointer) pair. This is incredibly useful, because delegates can:
1. call nested functions that need a pointer to the stack frame of the nestee function
2. call member functions that need `this` pointer
3. call lambdas
4. call COM member functions
The neato thing about this is the ABI for delegates is all the same, so a function that gets a delegate parameter will work with any of 1..4. It's one of the most used features of D.
But because ISA was mentioned, x86 does indeed even have native support for this: https://devblogs.microsoft.com/oldnewthing/20231211-00/?p=10... These instructions are not too useful though and I do not think anybody uses them.
Supporting closures, more or less, requires design decisions that are equivalent to choosing a specific layout for objects in an object-oriented language. C, as it is, makes none of these assumptions and you can translate a lot of different language ABIs into some C code (that may be clumsy). Keeping the abstraction that function pointers = pointers to entry points for functions, well, that’s frustrating for C programmers writing C programs, but extremely useful for interoperability.
> we simply need a code pointer and static chain pair
The code pointer and chain pair needs an object layout. You would have to pick a specific layout, and it would not be compatible with other languages that have a different layout.
Right now I can do this:
And I could call them: There is only one neutral, maximally compatible option here—which is to have the function pointer separate from the arguments you want to pass in, and pass them in explicitly.If you add closures to C you are making compatibility worse, not better.
Adding such a type as a vocabulary type would fix all this. You can argue that we fix an object layout for a pointer pair, but this seems an acceptable trade-off to me. This seems far from your previous claim that this "requires design decisions that are equivalent to choosing a specific layout for objects in an object-oriented language." Note also that such a type can always adapt to different calling conventions of other languages by using the address of a static thunk as code pointer so it is very generic.