lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


On Fri, Apr 24, 2020 at 02:07:24PM -0300, Roberto Ierusalimschy wrote:
> From the point of view of the standard, is all the same. As long as you
> keep the reference as a pointer to function (any function!), and cast
> it to the correct type when calling it, everything is guaranteed to work.

Typecasting a pointer to any function into a pointer to a function taking no argument may not be so obvious!
- "Any function" means that you can call a function that will take its parameters from registers and from the stack without limitation, so to call "any function" you need to prepare a stack frame, save and restore some registers: this could be forcibly done by the called function itself, or not at all (in which case it will be up to the caller to prepare the stack frame and save registers).
- But a function that takes no parameter could as well not use anything from the stack (it will create a stack frame itself, when needed, but the return address and the return value may be in registers that do not need to be preserved (except by the code of the called function itself where it is needed).

The effect on stack frames and registers can then differentiate the two types with different behavior at runtime.

If it is valid to call "any function" from a pointer, this means that important registers have always to be saved by the caller and the caller must then prepare the stack frame, before it can dereference the function pointer to call it, but if the callee does not use the stack for the return address, the return address pushed on the stack by the caller (during the preparation) will have no use by the function itself: the caller must also set the register where the function expects to find the return address.

Casting a function pointer taking no parameter (and then not necessarily using the stack) into a function taking any parameter is then valid in that case.

But the reverse is not true: if you typecast a function taking any parameters (which then saves registers prepares a new stack frame but expect to return to the addresse pushed on the stack by the caller, then the caller cannot just call the function by just passing the return address in a register, it must also push that return address on the stack.

Then comes in addition the calling conventions (different when you call a functions with "C" binding from a function with "C++" binding, plus other platform specific conventions like "fastcall", "stdcall", and their effect on the selection and ordering of registers used for parameters, or the possibility to pass a fixed or variable number of parameters...). This can become very tricky.

For this reason, GCC is correct by stating that typecasting a pointer to "any" function" into a pointer to function taking "specific parameter types" is unsafe (or could force the compiler to generate inefficient code for BOTH the caller and the callee (but the compiler actually does not necessarily know what kind of code the callee uses, if it is external to the compiled unit: this callee requires a specific declaration in some library header that the complier will see in order to correctly and safely compile the caller's code).

*Declarations* of external functions with "()" or with "(void)" or with "(...)" parameter lists are then not equivalent at all. I think that "()" should still mean the same as "(...)" i.e. it should mean a calling convention allowing a variable list of parameters like in traditional "C" code (which are known to have efficient calling methods than functions taking known parameter lists with fixed types, favored for C++ and to enforce strong type checking at compile time rather than by "safety checks" added ecxplicitly in the code of the function by the programmer, then compiled and kept in the generated code, then executed at runtime, the compiler being unable to perform type checking itself; and there's no good way for programmers to perform strong type checking of parameters in the code they explicitly add into their function, unless the compiler offers some help, by passing an additional parameter to the function for the "runtime type info" assumed by the caller when the caller was compiled: in C++, any RTTI mismatch detected by the callee would then generate a runtime typecheck exception)

Adding this RTTI info from the caller as one additional implicit parameter for the callee requires a new calling convention (not usable in classic K&R or ANSI "C" or even classic "C++", but that may be used in a new "C-safe" or a new "C++safe" convention)

This is not a stupid consideration: there are now processors that have very large number of internal registers, and that minimize the use of an external stack in memory as it is a known bottleneck (due to the limitation of the number and bandwidth of buses by which a CPU may interconnect with external memory or even with the attached internal caches, something that can be less limited with large sets of registers directly inside the CPU core where they could have a wide meshed interconnexion including with other cores with which they may exchange data directly without passing it by L2/L3 caches or external bus adapters for external memory).

These considerations may come from the recent progresses in massively parallel architectures, notably in GPU/APU (using little memory but lot of local registers and transfering data between these registers and external memory only by large blocks or by "passive sharing", during idle/waiting times where these CPU/APU can honor external attempts to read or write these registers, for these units, using an external stack has to be limited). And code compiled for these units may be very different, with specific calling conventions, that may not even be able to accept any call with a variable number of parameters (so a typecast from a classic "(*)()" to a restricted "(*)(void)" will even be invalid if the RTTI info is lost, as performing this call could crash instantly at runtime. To be really safe the classic "(*)()" pointer would not just store the pointer to the function entry point a pointer to an RTTI data structure generated by the compiler from the place the pointer was initially created, that RTTI structure containing the pointer to entry in the function code.

An externally compiled caller will also be generated with a similar RTTI structure containing the same entry point (resolved at link time) but the info that was known from any declaration of the target function, both RTTI structures will be compared by compiled callee. to assert their signatures match (the addresses of the two RTTI structures may be different).






_______________________________________________
lua-l mailing list -- lua-l@lists.lua.org
To unsubscribe send an email to lua-l-leave@lists.lua.org