user16658873
user16658873

Reputation:

How to declare assembly function with dynamic arguments in C++ like in C

I have some C code and I want to port it to c++, the problem is that in C++ I can't use an assembly function due to it's dynamic use

C version

extern asmFunc(); // C function prototype version

//actual use example
asmFunc(var1,ptr2,HANDLE); 
asmFunc(ptr4,var2,NULL,eg ...); //everything works

C++ version

extern "C" VOID asmFunc(); // C++ function prototype version

//actual use example
asmFunc(var1,ptr2,HANDLE); // E0140 too many arguments in function call
asmFunc(ptr4,var2,NULL,eg ...); // E0140 too many arguments in function call

The Assembly function is declared in a separate asm file and it uses direct syscalls from ntdll.dll's functions, that's why it requires dynamic arguments

How to make it work?

Upvotes: 1

Views: 339

Answers (2)

Peter Cordes
Peter Cordes

Reputation: 364458

You can have multiple labels at the same address. You could have multiple prototypes with different names that all happen to have the same address (and thus implemented by the same machine code). In your .asm file, you put:

global foo_int, foo_long, foo_float   ; NASM syntax for putting these in the symbol table
foo_int:
foo_long:
foo_float:
     your code goes here
     ret

You could also do this with assembler directives to create aliases for a symbol, like GAS .set foo_long, foo.

You can think of this as multiple names for the same function, or as other functions whose implementation is to fall through into the real function, as an optimized tailcall where you've even optimized away the jmp by making them contiguous in the asm.


Or using a GNU extension, declare multiple C or C++ names that all use the same asm symbol name. The compiler-generated .obj will have every call-site referencing the same symbol name. (Which you're setting fully manually, so even without extern "C" there's no name mangling and no leading underscore unless you choose one.)

// GNU C or C++;  compatible with the mainstream compilers other than MSVC
int foo_int(...) asm ("foo");
long foo_long(...) asm ("foo");
float foo_float(...) asm ("foo");

See it in action on Godbolt with clang and GCC, noting that return foo_float('a', 1); in the C++ source compiles to a call foo in the compiler-generated asm. (Godbolt compiles for Linux normally, but I used -mabi=ms to get GCC to use the Windows x64 calling convention.)


If this was in a separate DLL, each name would get resolved separately, wasting a bit of space for multiple DLL import entries (whatever Windows calls the functions pointers that are equivalent to the GOT in Linux dynamic linking.) The GNU C asm("name") way doesn't have this downside, as there's only one asm symbol name.
But if you're just linking this .asm file into the same executable or library as the C++ callers, separate symbol-table entries get resolved and go away at build time, not every time you run.


You could have a full prototype (and name) for every different function signature.

Or you could just have a name for every different return type,
using extern "C" int foo_int(...);
as Remy's answer shows. Note that (...) will trigger the default argument promotions, e.g. float will promote to double, so it's impossible to pass an actual float. (This is why printf's "%f" conversion takes a double.)

The promotions are harmless and very cheap for integer, though, just sign- or zero-extending to int. The mainstream x86 and x86-64 calling conventions were designed such that args are passed the same to variadic functions as they would be for a prototyped function with the same arg types (after promotion of float to double). x86-64 System V requires callers of variadic functions to set AL = # of XMM args, a number from 0 to 8; without any FP or vector args, this means each call-site will get an extra xor eax,eax. That's about as cheap as a 2-byte NOP, especially on Intel.


Assuming the return type is statically known for any given call-site, this should solve your whole problem. If not, it's trickier!

It wouldn't necessarily work to to return a union and decide at run-time which return value to use. Some calling conventions would return a pure float in a different register (e.g. xmm0) than they'd pick for a union with a float member. Even for pure integer, a union of an int and a larger struct would get returned in memory, differently from a plain int.

Upvotes: 0

Remy Lebeau
Remy Lebeau

Reputation: 596592

Use ... in the argument list to specify the function is a variadic function taking unspecified arguments, eg:

extern "C" VOID asmFunc(...); // C++ function prototype version

//actual use example
asmFUNC(var1,ptr2,HANDLE);
asmFUNC(ptr4,var2,NULL,eg);

Upvotes: 2

Related Questions