SRL Archive - Other Language Help and Tutorials

Function Pointers


Kevin

2013-05-08 16:36:20

Function Pointers: What are They and Why You Should Care


Foreword:
[indent]Why hello and welcome, everyone! This tutorial focuses on a concept that I have only rarely seen around here. Function Pointers are the idea of treating a procedure (or function) as a variable. That's a difficult concept for most to wrap their minds around, so I will dedicate a section just to try and explain and help you guys out.
I have seen a very rare few people implement function pointers, and only in cases where "procs" were already defined and the scripter simply redirected them. And even worse, I have never seen any sort of tutorial on them!
[/indent]

Table of Contents:
[indent]
  1. What Is a Function Pointer?
  2. When are they used now?
  3. How you can use them to Improve Script Efficiency!
  4. FAQ
[/indent]

What Is a Function Pointer?
[indent]A function pointer is simply another variable type that represents a place in script memory instead of a dedicated value. So you can treat this "variable" as a dedicated function to solve your problems.
Why should you care that you can do this instead of just calling the function you want to call? Occasionally, you don't know at compile time what function you want to call. "Well that's just ridiculous, Kevin. I always know what procedure I want to call and when." Wrong (or maybe you weren't thinking that, and in that case I'm sorry), and you will see why you're wrong next.
[/indent]

When are they used now?
[indent]My example will be the only case I have ever seen anyone else knowingly use a Function Pointer:
SRL_Procs[srl_OnRSUpdate]:= @SixHourFix;
Maybe you haven't seen that situation before, so I'll explain it for you. That right there is how our six hour fix currently works by default. SRLProcs[srl_OnRSUpdate] is a function pointer that will call whatever function it is pointing at when the six hour fix is necessary to be run. Using that you can redirect it to point at your own Six Hour Fix procedure (with a different name) to run your own code. The main thing to note with a function pointer is that when you are pointing your variable at a procedure, you must add an at symbol '@' prior to the name of the procedure you plan on pointing at. So let's give a very quick example based on the previous:

procedure RestartSmart;
begin
//Your custom smart restart code
end;

begin
SRL_Procs[srl_OnRSUpdate]:= @RestartSmart;
//Your normal code
end.
Well that's a fairly simple example, but it should give the basic syntax of existing scenarios for you. This still seems fairly limited though to most people, I mean outside of that one case, why should you care? Because it can improve the runtime of your script and cut down on 'if' and 'case' statements dramatically!
[/indent]

How you can use them to Improve Script Efficiency!
[indent]Ok, now I'm obviously just messing with you. I mean, how could this stupid and rare scenario ever matter to you? Because it can remove the necessity of 'if' or 'case' statements when your script allows multiple scenarios. Let's look at the idea of a basic AIO mining script. When you see one right now, the code is sorted roughly like this:

const
MineWhere = 'Lumbridge'; //Pick Lumbridge, Falador, or AlKharid

procedure MineLumbridge;
begin
//mine a rock at lumbridge
end;

procedure MineAlKharid;
begin
//mine a rock at Al Kharid
end;

procedure MineFalador;
begin
//mine a rock at Falador
end;

procedure BankLumbridge;
begin
//bank your ores in Lumbridge
end;

procedure BankAlKharid;
begin
//bank your ores in Al Kharid
end;

procedure BankFalador;
begin
//bank your ores in Falador
end;

begin
SetUpSRL;
DeclarePlayers;
LoginPlayer;
repeat
repeat
case MineWhere of
'Lumbridge':MineLumbridge;
'AlKharid':MineAlKharid;
'Falador':MineFalador;
end;
FindNormalRandoms;
if(InvFull)then
begin
case MineWhere of
'Lumbridge':BankLumbridge;
'AlKharid':BankAlKharid;
'Falador':BankFalador;
end;
end;
until(not loggedIn);
until(not logInPlayer);
end.
Well, that is one possible setup without function pointers, there could be entire methods that handle the whole looping scenario for each city that ends up wasting a lot of re-usable code space. So how could we possibly enchance this with a Function Pointer? Easily, like this:

const
MineWhere = 'Lumbridge'; //Pick Lumbridge, Falador, or AlKharid

var
Mine, Bank: procedure();
procedure MineLumbridge;//procedures were all declared like normal
begin
//mine a rock at lumbridge
end;

procedure MineAlKharid;
begin
//mine a rock at Al Kharid
end;

procedure MineFalador;
begin
//mine a rock at Falador
end;

procedure BankLumbridge;
begin
//bank your ores in Lumbridge
end;

procedure BankAlKharid;
begin
//bank your ores in Al Kharid
end;

procedure BankFalador;
begin
//bank your ores in Falador
end;

begin
SetUpSRL;
DeclarePlayers;
LoginPlayer;
case MineWhere of
'Lumbridge':begin
Mine:= @MineLumbridge;//Setting the variable is where you see that '@'
Bank:= @BankLumbridge;
end;
'AlKharid':begin
Mine:= @MineAlKharid;
Bank:= @BankAlKharid;
end;
'Falador':begin
Mine:= @MineFalador;
Bank:= @BankFalador;
end;
end;
repeat
repeat
Mine();//Finally when you call it, you must add the parenthesis even if you aren't passing it any variables! It's a syntax requirement.
FindNormalRandoms;
if(InvFull)then
Bank();
until(not loggedIn);
until(not logInPlayer);
end.
Not only was this code way smaller, but now we got rid of those case statements - thus improving the scripts efficiency! I hope this has helped you and I hope to see these used more often in the future!

For a far more realistic and useful example, feel free to look here: http://villavu.com/forum/showthread.php?t=104249
[/indent]

FAQ:
[indent]Ask me some questions and I'll toss them up here.
[/indent]

Janilabo

2013-05-08 16:47:46

Awesome little tutorial on function pointers, Kevin!
I found this really useful, thanks. :)

Kevin

2013-05-08 17:05:50

From: Janilabo
Awesome little tutorial on function pointers, Kevin!
I found this really useful, thanks. :)


Thank you! I hope it helps somehow and the appreciation has an extra kicker coming from a SSRL :)

masterBB

2013-05-08 18:26:36

Nice tut. I am sure it will help some people!

Just a few things:

It would be nice/better if a var/const holding a funct/proc got something in it's name to indicate this. mineProc or mineF for example.

In SRL 6 this won't work anymore. The syntax will change slightly and the new way of setting a function to a var would be simply be: mineProc := MyMineFunction;

rj

2013-05-08 20:14:13

Good guide!

toxjq

2013-05-31 01:48:37

learned about pointers back when i was using c++, this was a good reminder of the power of pointers :)

Chris!

2013-05-31 02:22:14

Beautiful layout and guide, Kevin! Well done.

cause

2013-07-25 20:54:36

I was just checking out your sig since your new cups…very nice tut, did not know how to use these. Btw speaking of efficiency, is it more efficient in a literal sense of faster compile/run time, or just more efficient in terms of coding[tidiness]? @Kevin

Nebula

2013-07-25 21:18:24

I have never needed to use these… why? cause i'm cool and i know a better way.


CallProc('mine' + minewhere, TVariantArray);
CallProc('bank' + minewhere, TVariantArray);

now where's my tut cup, cause i'd consider that a tut on using callproc^

Kevin

2013-07-25 21:25:12

From: cause
I was just checking out your sig since your new cups…very nice tut, did not know how to use these. Btw speaking of efficiency, is it more efficient in a literal sense of faster compile/run time, or just more efficient in terms of coding[tidiness]? @Kevin

Depending on the situation, both. It's more efficient simply in tidiness by usually taking up less space, and in any situation when you will repeatedly (or at least a few times) follow the same procedure path, it is a faster run time due to the direct call of the desired function with no possible if checks in the meanwhile. These have other uses as well, this just happens to be the most common application, with another application being like this: http://villavu.com/forum/showthread.php?t=104249
From: Nebula
I have never needed to use these… why? cause i'm cool and i know a better way.


CallProc('mine' + minewhere, TVariantArray);
CallProc('bank' + minewhere, TVariantArray);

now where's my tut cup, cause i'd consider that a tut on using callproc^

Function pointers are faster than CallProc :p And besides, if anything since this is my tut page, you adding that should give me the tutorial cup ;)

footballjds

2014-01-13 16:37:52

kevin,

thanks for the tut. But i don't think it is needed in this use-case. Why not have your banking function decide what bank to use based on a const integer that the use set?

Kevin

2014-01-13 16:56:59

From: footballjds
kevin,

thanks for the tut. But i don't think it is needed in this use-case. Why not have your banking function decide what bank to use based on a const integer that the use set?


Because it can be done in things that change mid-script run. As a more complicated example, I had that AIO pickpocketer that changed targets as you levelled up, and as opposed to 8 extra checks every single loop, at the desired level, it would change the pointers once and be done.
Similarly it was helpful in Slayer when the pointers are changed once per new monster as opposed to checking those every time you needed to do something specific for one.

bg5

2014-01-13 19:08:33

They are called *function* pointers, but you don't pass functions ,but only procedures to it? :garfield:

Nice tut, but maybe add something about CallProc() too? As it enables to pass arguments to function…

Brandon

2014-01-13 19:12:28

From: Kevin
Because it can be done in things that change mid-script run. As a more complicated example, I had that AIO pickpocketer that changed targets as you levelled up, and as opposed to 8 extra checks every single loop, at the desired level, it would change the pointers once and be done.
Similarly it was helpful in Slayer when the pointers are changed once per new monster as opposed to checking those every time you needed to do something specific for one.


Nice tutorial but I felt a bit mislead?

I would not use a function pointer like that. However, such statements or comparisons between using a const/enum and a switch vs. a function pointer in terms of efficiency is irrelevant and micro-optimisation.

For complex algorithms or callbacks, a function pointer is best used. I've only found myself ever using function pointers in callbacks or algorithms like below once:


type Comparator = Function (var A, B): Boolean;


Procedure Sort(var Arr: TStringArray; Func: Comparator);
var
I: Integer;
T: String;
begin
If (@Func = nil) Then Exit;

For I := 0 To High(Arr) - 1 Do
If (Func(Arr[I], Arr[I + 1])) Then
Begin
T := Arr[I + 1];
Arr[I + 1] := Arr[I];
Arr[I] := T;
I := -1;
End;
end;


Function SmallToLarge(var A, B: String): Boolean;
Begin
Result := Length(A) > Length(B);
End;

Function LargeToSmall(var A, B: String): Boolean;
Begin
Result := Length(A) < Length(B);
End;

Function Alphabetically(var A, B: String): Boolean;
var
I, M: Integer;
Begin
Result := True;
M := Min(Length(A), Length(B));

For I := 1 To M Do
If (A[I] <= B[I]) Then
Exit(False);
End;

var
Arr: TStringArray;
begin
Arr := ['a', 'c', 'dde', 'fdgsgsgs', 'sfds', 'd', 'A', 'B'];

Sort(Arr, @SmallToLarge);
Sort(Arr, @Alphabetically);
Sort(Arr, nil);
writeln(Arr);
end.

In the above example, Comparator is a function pointer with un-typed parameters. Its parameters can be of type int or string or whatever you want to compare. Next we have a sort function that uses that comparator to determine when it should swap indices in an array.
I made the array parameter strongly typed TStringArray because I wasn't sure how to index an untyped variable without an explicit cast.

Finally, you just write as many comparators as you wish. You never have to re-write your sort algorithm though. Simply write a function comparing a to b and the sort algorithm takes care of business regardless.

The advantage here is that you can sort many different types in many different ways without ever touching your sort algorithm.



Not only was this code way smaller, but now we got rid of those case statements - thus improving the scripts efficiency!


No it doesn't. A function pointer requires an extra deference and can be a couple cycles slower than a simple branch. A function pointer is almost always coupled with branching because you need to choose when to call or pass said function and believe it or not, you know exactly when to do that at compile time. Notice that in both your examples they still use a case statement. However, the first example is more efficient because it doesn't need to dereference any pointers. Just a direct call. Your second example is exactly the same as the first but you're now dereferencing a pointer. Not only that but in order to even pass/assign the functions, you must also take the address of them as well using the @ address-of operator. I'm still inclined to say they are similar or both exactly the same speed. The difference is negligible/non-existent. You won't notice anything at all.

Imo the only advantage is in callbacks.

Again nice tutorial and it's fairly unique as I haven't seen anything on function pointers lately. I think you should get a cup still.


Here's a another (unsafe casting trick) I found today..


type Comparator = Function (var A, B): Boolean;


Procedure Sort(var Arr: TIntegerArray; Func: Comparator);
var
I: Integer;
T: Integer;
begin
If (@Func = nil) Then Exit;

For I := 0 To High(Arr) - 1 Do
If (Func(Arr[I], Arr[I + 1])) Then
Begin
T := Arr[I + 1];
Arr[I + 1] := Arr[I];
Arr[I] := T;
I := -1;
End;
end;


Function SmallToLarge(var A, B: String): Boolean;
Begin
Result := Length(A) > Length(B);
End;

Function LargeToSmall(var A, B: String): Boolean;
Begin
Result := Length(A) < Length(B);
End;

Function Alphabetically(var A, B: String): Boolean;
var
I, M: Integer;
Begin
Result := True;
M := Min(Length(A), Length(B));

For I := 1 To M Do
If (A[I] <= B[I]) Then
Exit(False);
End;

Function ISmallToLarge(var A, B: Integer): Boolean;
Begin
Result := A > B;
End;

var
Arr: TStringArray;
Arr2: TIntegerArray;
begin
Arr := ['a', 'c', 'dde', 'fdgsgsgs', 'sfds', 'd', 'A', 'B'];
Arr2 := [9, 10, 11, 8, 7, 3, 12, 15, 6, 4, 5, 13, 14, 2, 1];

Sort(TIntegerArray(Arr), @SmallToLarge);
Sort(TIntegerArray(Arr), @Alphabetically);
writeln(Arr);

Sort(Arr2, @ISmallToLarge);
writeln(Arr2);
end.

That same sort algorithm now accepts a TIntegerArray for sorting integers but I casted a TStringArray to a TIntegerArray and it sorted it just fine without slicing! Lol.. Doesn't work for records though.

footballjds

2014-01-13 20:37:59

can you set the procedure to null so it does nothing?
does this work with lape and pascal?
does this work with OSRS and RS3?(can it be in both sections of tuts?)

Kevin

2014-01-15 16:45:20

From: Brandon
Nice tutorial but I felt a bit mislead?

I would not use a function pointer like that. However, such statements or comparisons between using a const/enum and a switch vs. a function pointer in terms of efficiency is irrelevant and micro-optimisation.

…
I was trying to give some basic examples although this may be a better use case scenario for people that I didn't include in OP, but just a later comment: http://villavu.com/forum/showthread.php?t=104249 (I'll add to OP on that note).

As for efficiency, yes both scenarios have a case, however the function pointer scenario only runs the case once (in the original examples), and as both scenarios will have the guaranteed jump/branch scenario, relying on the dereferencing followed by a guaranteed jump should be faster than a comparison leading to a branch (possibly) and then to a jump.

Although, I do definitely agree that you should not notice any difference in any reasonable scenario. We are discussing individual computer cycles per loop (nanoseconds). The main advantage in these scenarios is I feel it looks far cleaner.

As for any cup, I don't think this is really deserving of anything beyond what I have =/

As for your examples, would you mind if I add the first one (with credits) to the OP?

From: footballjds
can you set the procedure to null so it does nothing?
does this work with lape and pascal?
does this work with OSRS and RS3?(can it be in both sections of tuts?)

Hm… I dunno what happens when a procedure is set to null in these langauges. Probably throws an exception, although I'm unsure…
It does work in pascalscript, it should work in lape.
Technically no reason it can't work with both OSRS and RS3. As I'm looking at it… I never put this in the OSR forums, this was supposed to be in general forums as it is concepts that don't have to apply to RS… I wonder if that happened during the re-org. I'll try and have it moved.


Edit:
@Ashaman88, hey there buddy old pal. Would you care to move this to a more generic area? Perhaps something like here: http://villavu.com/forum/forumdisplay.php?f=501 (or maybe another better area that isn't coming to mind offhand).

Brandon

2014-01-15 17:37:20

From: Kevin
I was trying to give some basic examples although this may be a better use case scenario for people that I didn't include in OP, but just a later comment: http://villavu.com/forum/showthread.php?t=104249 (I'll add to OP on that note).

As for efficiency, yes both scenarios have a case, however the function pointer scenario only runs the case once (in the original examples), and as both scenarios will have the guaranteed jump/branch scenario, relying on the dereferencing followed by a guaranteed jump should be faster than a comparison leading to a branch (possibly) and then to a jump.

As for your examples, would you mind if I add the first one (with credits) to the OP?


I don't mind you adding anything. It is your thread. As for the efficiency, consider that you have this code:


repeat
repeat
case MineWhere of //branch every loop.
'Lumbridge':MineLumbridge; //jmp every loop.
'AlKharid':MineAlKharid;
'Falador':MineFalador;
end;
until(not loggedIn);
until(not logInPlayer);

and comparing it to:


case MineWhere of
'Lumbridge':begin //branch once at the start of the script
Mine:= @MineLumbridge; //address of + assignment once at the start of the script
end;
'AlKharid':begin
Mine:= @MineAlKharid;
end;
'Falador':begin
Mine:= @MineFalador;
end;
end;
repeat
repeat
Mine(); //dereference + jmp every loop.
until(not loggedIn);
until(not logInPlayer);


In that case, option 2 with the function pointer is indeed more efficient because you did only call the branch code ONCE at the start of the script. Which is cleaner though? The first option looks far cleaner to me :S


However, consider now that the test is fair below:


var
m: Integer;


case MineWhere of
'Lumbridge':begin //branch once at the start of the script
m:= 1;
end;
'AlKharid':begin
m := 2;
end;
'Falador':begin
m := 3;
end;
end;

repeat
repeat
case m of //branch + jmp every loop.
1: mineLumby;
2: mineAlk;
3: mineFal;
end;
until(not loggedIn);
until(not logInPlayer);


versus:



case MineWhere of
'Lumbridge':begin //branch once at the start of the script
Mine:= @MineLumbridge;
end;
'AlKharid':begin
Mine:= @MineAlKharid;
end;
'Falador':begin
Mine:= @MineFalador;
end;
end;
repeat
repeat
Mine(); //dereference + jmp every loop.
until(not loggedIn);
until(not logInPlayer);

Now that the script is fixed, it is now fair to say that a `branch + jmp` every loop is comparable to a `dereference + jmp` since Simba cannot optimise itself.

However, +1 from me for cleanliness. I didn't exactly see it in the OP but I see it now.


Now if you really want the first one to out perform the second one via micro-optimisation, you can simply optimise both algorithms as follows:

Function Pointer:

case MineWhere of
1:begin //branch once at the start of the script
Mine:= @MineLumbridge;
end;
2:begin
Mine:= @MineAlKharid;
end;
3:begin
Mine:= @MineFalador;
end;
end;
repeat
repeat
Mine(); //dereference + jmp every loop.
until(not loggedIn);
until(not logInPlayer);


Enumeration:

repeat
repeat
case mineWhere of //branch + jmp every loop.
1: mineLumby;
2: mineAlk;
3: mineFal;
end;
until(not loggedIn);
until(not logInPlayer);

This removes the first case statement and the string comparison.. Just a single branch and jmp every loop whereas the first one must first be assigned a pointer via address of then it can dereference + jmp.

Not only is the Non-Function pointer cleaner but it is also faster by a couple instructions at the start of the script. Runtime speed is the same.


Benchmark:

var
E: Integer;
P: Procedure;


Procedure Proc;
begin
Sleep(10);
end;

Procedure Pointers;
begin
case E of
1: P := @(Proc);
end;
end;

Procedure NonePointer;
begin
case E of
1: Proc();
end;
end;

var
I, T: Integer;
begin
E := 1;

T := GetSystemTime;
Pointers;

For I := 0 To 1000 do
begin
P();
end;

writeln('Time: ', ((GetSystemTime - T) - 10000) / 1000); //1.016;

T := GetSystemTime;

For I := 0 To 1000 do
begin
NonePointer();
end;

writeln('Time: ', ((GetSystemTime - T) - 10000) / 1000); //1.015
end.

Lol. 1ms doesn't count. The nonpointer wins by a hair lol.. EDIT: Ran it multiple times. Both average out at 1.015 - 1.016 in the above run (lowest for both was actually 1.000).

Anyway, since the Non-Pointer is clean, I personally will be using that and only using pointers where really necessary such as in your link or my sort.

Ashaman88

2014-01-15 17:41:32

From: Kevin
I was trying to give some basic examples although this may be a better use case scenario for people that I didn't include in OP, but just a later comment: http://villavu.com/forum/showthread.php?t=104249 (I'll add to OP on that note).

As for efficiency, yes both scenarios have a case, however the function pointer scenario only runs the case once (in the original examples), and as both scenarios will have the guaranteed jump/branch scenario, relying on the dereferencing followed by a guaranteed jump should be faster than a comparison leading to a branch (possibly) and then to a jump.

Although, I do definitely agree that you should not notice any difference in any reasonable scenario. We are discussing individual computer cycles per loop (nanoseconds). The main advantage in these scenarios is I feel it looks far cleaner.

As for any cup, I don't think this is really deserving of anything beyond what I have =/

As for your examples, would you mind if I add the first one (with credits) to the OP?


Hm… I dunno what happens when a procedure is set to null in these langauges. Probably throws an exception, although I'm unsure…
It does work in pascalscript, it should work in lape.
Technically no reason it can't work with both OSRS and RS3. As I'm looking at it… I never put this in the OSR forums, this was supposed to be in general forums as it is concepts that don't have to apply to RS… I wonder if that happened during the re-org. I'll try and have it moved.


Edit:
@Ashaman88, hey there buddy old pal. Would you care to move this to a more generic area? Perhaps something like here: http://villavu.com/forum/forumdisplay.php?f=501 (or maybe another better area that isn't coming to mind offhand).


Would love to but I has no power here :(

@Justin

Justin

2014-01-15 20:50:23

From: Ashaman88
Would love to but I has no power here :(

@Justin


Done.