4 ms·
> Sure, you can always map your requirement onto the standard calling convention somehow, but if you can't directly call out or in anyway it's a constraint it's
by lelanthran 3y ago
> Sure, you can always map your requirement onto the standard calling convention somehow, but if you can't directly call out or in anyway it's a constraint it's totally reasonable to not want to be forced into.
I get that, but what I don't get is what about C makes it harder to define your own calling convention than defining it in an assembler language.
In the context of this thread, OP wants a portable assembler. Implementing a custom calling convention by emitting any assembly language is bound to be more work than implementing a custom calling convention by emitting C.
Maybe the situation regarding floating point registers when passing floating point values? That's about the only thing I can think of that makes it harder to emit C code that implements a custom calling convention than doing it in assembler, but even for that I can think of workarounds, just so that other high-level constructs can be used.
- vidarh 3y ago> I get that, but what I don't get is what about C makes it harder to define your own calling convention than defining it in an assembler language. You don't get to control which registers are caller or callee saved, or which registers are free to use. If your desired calling convention fits within those constraints, you can use it via C all you want. If it doesn't, you usually will need to either resort to compiler extensions or give up on using C. Whether it's wise to change that for your given use case can be a worthwhile question to ask, but that is another issue. Ironically sometimes this choice is about making C interop easier. E.g. I have a prototype Ruby compiler. It can call out to C from a low level "escape". The same exact code is used to generate calls to Ruby methods and to C. But the Ruby code needs the arity of the code that's being called to be passed along. Putting it in a register that's not used for arguments in the standard calling convention means that when calling out to e.g. glibc the C code just won't be able to access those values. Now, in my case, if I'd been on the fence about using C as the target, then I'd have handled that in a different way instead, so that didn't really influence my choice to emit assembler. But I can't define that calling convention in ISO C. > Implementing a custom calling convention by emitting any assembly language is bound to be more work than implementing a custom calling convention by emitting C. Generally, your calling convention simply involves what goes into registers and what goes on the stack in which order. Storing to a register vs. outputting to an argument list only marginally different in effort. In the compiler mentioned above, preparing arguments is literally: arges.each_with_index { a,i| @e.save_to_stack( compile_eval_arg(scope, a), i) } If I was emitting to a C argument list instead, the only thing that'd change is the `@e.save_to_stack` bit. `save_to_stack` is a one-line method. The non-C compatible parts add up to 2-3 more lines in that case. One doing something more unusual might be longer, but not a lot. There is plenty of complexity of dealing with assembler directly, and you certainly ought to have a reason for doing so over outputting to a higher-level target, but the complexity cost of assembler is not there. > OP wants a portable assembler And as they pointed out, C is not a portable assembler. It is sometimes close enough, but it imposes plenty of restrictions that you can't get around in ISO C. That can be perfectly fine, but it's a tradeoff. Sometimes you can find a halfway house of requiring a specific compiler, but that also removes at least some of the advantage.
- lelanthran 3y agoYou make some very good points, and I agree with your conclusion, viz it's a trade-off. I'm still wondering if the OP (@duped) meant the same thing as you did, or if he had another use-case in mind when he said "calling conventions". FWIW ... > But the Ruby code needs the arity of the code that's being called to be passed along. Putting it in a register that's not used for arguments in the standard calling convention means that when calling out to e.g. glibc the C code just won't be able to access those values. In general, when a call returns you can't trust the register contents for those registers that are unused in the calling convention, because the callee might clobber it (for example by calling back into your language before returning). What I did[1] when doing the ffi to the external C functions was emit a `setjmp()`, call the function, and emit `longjmp()` when the callee returned. That allowed recursive calls no matter how many times that FFI boundary is crossed in a single function call: The language calls into C API, which calls something that invokes a callback in the language, which makes another call to a C function, etc. [1] IOW, this is not the usual way, as far as I know. All a bit hackier than when I (for a different toy language) used `libffi` when emitting the code to call external C functions, which (IIRC) makes certain gaurantees about some of the registers.
- vidarh 3y ago> In general, when a call returns you can't trust the register contents for those registers that are unused in the calling convention, because the callee might clobber it (for example by calling back into your language before returning). Depends on the register and the calling convention. E.g. for i386 the Intel ABI requires that eax, edx, and ecx are caller-saved, and so can be clobbered by the called function, but the rest are callee-saved, and so you can rely on them surviving just fine if you're calling any code generated by a compiler conforming to the Intel ABI. So calling out to C-code is fine. (Calling back into the Ruby code requires extra effort, because the C code can't know how to set the arity; I might change this to store the arity of a method so that it can be looked up by the caller, in which case it might be doable to not require setting it at the time of the call, but that increases the amount of code at each call site as you'd need to resolve the method address and look it up relative to the actual method address instead of an indirect call, in order to avoid race conditions if someone redefines a method (we do love redefining methods at runtime in Ruby; it doesn't actually happen that often, but it can in theory happen at any time, and proving it won't for a given piece of code is a pain); it might be worth it at some point, even optionally, as when you can safely cache the method address you'd be able to jump straight past the arity check if you can guarantee the right number of arguments, but it's not simple).