Function pointers

Blitz3D Forums/Blitz3D Programming/Function pointers

So, since I'm currently messing around with Lua, I got interested, if it's possible to find out the pointer that points to a user-defined Blitzbasic function.

It should be possible, since every time, when Blitz calls a function, it pushes the current instruction pointer onto the stack, so that the called function is able to return to this point.
So, imagine, you wrote a function in C in combination with inline assembler, that is called by your Blitz function right after the function itself is called.
The C function then reads the instruction pointer, which was pushed by Blitz onto the stack, and gets a value that points to the instruction right after the Blitz function calls the C function.
Everything you have to do now is to subtract a small value from this instruction pointer and you get... the pointer to the Blitz function!

I tried it out and it actually works - but there're still problems.
It seems that Blitz uses a different calling convention when calling Blitz functions than the _stdcall convention.
That leads to a memory access violation, when the Blitz function tries to return to its caller.

Global Pointer

Test()
CallFunction( Pointer ) ;<--- This is the C function that calls the Blitz function by it's pointer
WaitKey()
End

Function Test()
	Pointer = FunctionPointer() ;<--- This is the C function that returns the function pointer
	
	Print "This works! Function pointer: " + Pointer

	Return  ;<--- This works fine when the function is called by Blitz
	;however, when the DLL calls this function, 'Return' leads to a MAV
End Function



I found a way to avoid this, but it's not as clean as I want it to be.
I rewrote 'CallFunction' so it remembers from where it was called (using the instruction pointer on the stack).
The Blitz function then calls a C function 'ReturnInt' instead of 'Return', when it's called by the DLL.
'ReturnInt' then jumps back to the instruction pointer saved by 'CallFunction'.
This actually works, but it leaves a really messy stack behind, which leads to a stack overflow, if you let the DLL call the Blitz function too often.

So, is there anyone out there who knows, how a Blitz function has to be called or which convention it uses?
It would be very nice to get the function pointers to work, especially for DLLs that require Callback functions or for example Lua, which would call user-defined Blitz functions, when used in the script.

My current test code in Blitz:
Global Pointer, Call

Print Test()
CallFunction( Pointer )
WaitKey()
End

Function Test()
	Pointer = FunctionPointer()
	
	Print "Works! Call no. " + Call
	
	Call = Call + 1
	
	If Call = 1 Then
		Return 5
	Else
		ReturnInt( 5 )
	EndIf
End Function


C++ - Code:
#define BBEXPORT extern "C" _declspec(dllexport)

void (__stdcall *JumpFunction)(void);

BBEXPORT int __stdcall FunctionPointer() {
	unsigned int StackPosition, Adress;

	__asm {
		mov StackPosition,esp
		mov esp,ebp
		add esp,4
		pop Adress
		mov esp,StackPosition
	}

	return Adress - 8;
}

BBEXPORT int __stdcall CallFunction( int (__stdcall *B3DFunction)(void) ) {
	unsigned int StackPosition;

	__asm {
		mov StackPosition,esp
		mov esp,ebp
		add esp,4
		pop JumpFunction
		mov esp,StackPosition
	}

	return B3DFunction();
}

BBEXPORT void __stdcall ReturnInt( int Value ) {
	__asm {
		mov eax,[Value]
	}

	JumpFunction();
}


This is a very interesting read. Thank you. So, at this point, this is like a special form of Gosub ? You could use it not only for functions, but you can use it at any point in your program, right ?
I have not much experience with C++, which compiler do you use ? My guess would be you should use cdecl, since userlib functions should be declared that way as well.

So, at this point, this is like a special form of Gosub ? You could use it not only for functions, but you can use it at any point in your program, right ?


Yes, that's right.
You could use it at any point in your program, save the pointer and use this pointer to jump to that piece of code later.

Thank you for the hint with Gosub - this was the solution for my problem!
It seems that when you create a label, save the pointer to this label and jump from the DLL to that label, you're able to jump back with 'Return'.

So, the following code works:
Global Pointer
GoSub Test

CallFunction( Pointer )
WaitKey()
End

.Test
	Pointer = FunctionPointer()
	Print "Hello, I am called from both Blitz and C!"
Return ;<---- With GoSub instead of Function, 'Return' works without a Memory Access Violation


The only problem ist that you can't use either types or string variables, when the code is called from the DLL.
But with a bit of a workaround, you can get this to work too.

Global Pointer, FirstCall = True
GoSub Test

CallFunction( Pointer )
WaitKey()
End

.Test
	Pointer = FunctionPointer()
	
	If FirstCall Then
		FirstCall = False
		Return
	Else
		B3DFunction()
	EndIf
Return

Function B3DFunction()
	StringVar$ = "This function can handle strings without a problem!"
	
	Print StringVar
End Function

Meaning, if you let the code after the label call a regular Blitz function, everything works fine.

So, with this DLL, we finally have the feature of function pointers in Blitz :)
A bit messy though, but working.

I have not much experience with C++, which compiler do you use ?

I use Visual C++ 2008 Express Edition (long name :/).

My guess would be you should use cdecl, since userlib functions should be declared that way as well.

I already tried all kinds of calling conventions, but none would work.
And my Userlibs.txt says "All functions MUST use the _stdcall calling convention.", so I ended up with that.

It's only a guess: The Return Adress is stored on the Stack, just like the parameters. The function caller writes everything to the stack (first the return address, then the parameters), then calls the function. The function reads "down" the stack, including and finally the return adress (filo). I still have no idea where the result is stored (eg a=test(b)), esp. because the result may also be a string or float... At the end the Stackpointer might have to be corrected, so there won't be any stack under- or overflow.

But as I said in the beginning: this was only a guess. From somebody who didn't touch assembler for years.