Skip to content

Latest commit

 

History

History
821 lines (653 loc) · 22.4 KB

features.md

File metadata and controls

821 lines (653 loc) · 22.4 KB

Features

Using Callbacks

"y_inline" doesn't actually have to accept inline functions:

#include <YSI_Coding\y_inline>

public Response(playerid, dialogid, response, listitem, string:inputtext[])
{
	#pragma unused dialogid, inputtext
	
	if (response)
	{
		va_SendClientMessage(playerid, COLOUR_MSG, "You picked: %d", listitem);
	}
	else
	{
		SendClientMessage(playerid, COLOUR_MSG, "You pressed cancel");
	}
}

CMD:pick(playerid, params[])
{
	// Called when the player responds to the dialog.
	Dialog_ShowCallback(playerid, using public Response<iiiis>, DIALOG_STYLE_LIST, "Pick a number", "0\n1\n2\n3\n4", "OK", "Cancel");
	return 1;
}

The major difference is in the type safety. Here Dialog_ShowCallback takes a function with 4 integer parameters and one string parameter. If Response were an inline, the compiler could determine that the parameters were correct automatically. However, since this is a public, we must tell the compiler that this is correct. Hence the <iiiis> after the function name.

Closures

When you are in an inline, the variables from the enclosing function are available:

#include <YSI_Coding\y_inline>

CountFives(array[], size)
{
	new count = 0;
	inline IsFive(value)
	{
		if (value == 5)
			++count;
	}
	ForEach(array, using inline IsFive, size);
	return count;
}

Each time IsFive is called (once per array element), count has kept its value from the last call, so this will correctly increment. When ForEach ends, the count value needs to be correctly written back to the calling function via Callback_Restore():

ForEach(array[], Func:callback<i>, size)
{
	for (new i = 0; i != size; ++i)
	{
		@.callback(array[i]);
	}
	Callback_Restore(callback);
}

const

Modifying variables in closures, and keeping them updated for multiple calls, is a non-zero amount of code. If you don't modify them ever, then you can skip the restoration with inline const:

#include <YSI_Coding\y_inline>

CountFives(array[], size)
{
	new count = 0;
	inline const IsFive(value)
	{
		if (value == 5)
			++count;
	}
	ForEach(array, using inline IsFive, size);
	return count;
}

Even if ForEach is not modified, this will ALWAYS return 0 - because count is modified inside IsFive, but the new value is instantly forgotten when the current call ends.

Callback_Restore

Callback_Restore copies the closure data back in to the original stack. This is only useful when the inline isn't const (so there is modified data to copy) and the inline was instantly called (so the original stack still exists). This is no use for callbacks or timers, because the original function ended before the inline was called, thus calling this will probably just corrupt random memory.

Non-const with Callback_Restore:

CallAndCopy(Func:x<>)
{
	for (new i = 0; i != 10; ++i)
	{
		@.x();
	}
	Callback_Restore(x);
}

Test()
{
	new a = 1;
	inline X()
	{
		printf("%d", a); // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
		++a;
	}
	CallAndCopy(using inline X);
	printf("%d", a); // 11
}

Note that the value of a is modified and persisted, even after the calls to the inline have ended.

Non-const without Callback_Restore:

CallNoCopy(Func:x<>)
{
	for (new i = 0; i != 10; ++i)
	{
		@.x();
	}
}

Test()
{
	new a = 1;
	inline X()
	{
		printf("%d", a); // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
		++a;
	}
	CallNoCopy(using inline X);
	printf("%d", a); // 1
}

In this case, the value of a still increments within the inline, because the lack of const keeps the current value within the internal data memory for this function. However, because Callback_Restore is never called, it is never copied back to the main stack at the end.

const with or without Callback_Restore:

CallIrellevantCopy(Func:x<>)
{
	for (new i = 0; i != 10; ++i)
	{
		@.x();
	}
	Callback_Restore(x); // Irrelevant
}

Test()
{
	new a = 1;
	inline const X()
	{
		printf("%d", a); // 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
		++a;
	}
	CallIrellevantCopy(using inline X);
	printf("%d", a); // 11
}

With const, the closure is never persisted, so the value of a never updates between calls. The presense of Callback_Restore in that code WILL copy the data back in to the stack, but this is just copying data over identical data. However, the call-site of an inline will not always know that the declaration was const, so this is not entirely pointless (because the source may also NOT be const).

Manipulation

Function handlers are just variables. They can be stored, passed, and more:

new Func:g_cb<>;

CalleeA(Func:cb<>)
{
	@.cb();
}

CalleeB()
{
	CalleeA(g_cb);
}

CalleeC(Func:cb<>)
{
	g_cb = cb;
	CalleeB();
}

Caller()
{
	inline Called()
	{
		print("hi");
	}
	CalleeC(using inline Called);
}

But don't forget closure rules - the data will be invalid after CalleeC ends (which in this example is after the other two callees end) unless you explicitly claim it.

Type Safety

Lets write a Fold function - this takes a current array element and a running total, and does something to them. So the function that gets called (inline, public, or other) needs two integer parameters - current and accumulated. So we specify that Fold takes a function that takes two parameters using Func:name<ii>:

Fold(array[], Func:callback<ii>, initial, size = sizeof (array))
{
	for (new i = 0; i != size; ++i)
	{
		initial = @.callback(array[i], initial);
	}
	Callback_Restore(callback);
	return initial;
}

The new @. syntax is used to call the function defined by callback. Fold is called like so:

#include <YSI_Coding\y_inline>

CountFives(array[], size = sizeof (array))
{
	inline const IsFive(value, count)
	{
		if (value == 5)
			return count + 1;
		return count;
	}
	return Fold(array, using inline IsFive, 0, size);
}

Here IsFive can safely be const because no closure variables are modified.

inline_return

This is a work-around for a compiler limitation:

#include <YSI_Coding\y_inline>

CountFives(array[], &total, size = sizeof (array))
{
	inline const IsFive(value, count)
	{
		if (value == 5)
			return count + 1;
		return count;
	}
	total = Fold(array, using inline IsFive, 0, size);
}

Here the inline function contains return, but the outer function doesn't. Due to a known issue*, inline and outer functions must both return a number, or the inline function can return nothing. If you want an inline function to return a number, but the outer function to return something else (say a string, or nothing), you need inline_return instead:

#include <YSI_Coding\y_inline>

CountFives(array[], &total, size = sizeof (array))
{
	inline const IsFive(value, count)
	{
		if (value == 5)
			inline_return count + 1;
		inline_return count;
	}
	total = Fold(array, using inline IsFive, 0, size);
}

Deferred Calls

Given the following code:

#include <YSI_Coding\y_inline>

CountFives(array[], size)
{
	new count = 0;
	inline const IsFive(value)
	{
		if (value == 5)
			++count;
	}
	ForEach(array, using inline IsFive, size);
	return count;
}

CountFives is the caller function, ForEach is the callee function, and IsFive is called function. CountFives calls ForEach, ForEach calls IsFive (possibly multiple times). All the calls to IsFive must happen before ForEach ends (i.e. before return count; is run). But what about timers and callbacks? For example:

forward InlineTimerCall(Func:tt<>);

public InlineTimerCall(Func:tt<>)
{
	@.tt();
}

SetInlineTimer(Func:tt<>, delay, bool:repeat)
{
	SetTimerEx("InlineTimerCall", delay, repeat, "i", _:tt);
}

PrintLater(a, b, c)
{
	inline const PrintNow()
	{
		printf("a = %d, b = %d, c = %d", a, b, c);
	}
	SetInlineTimer(using inline PrintNow, 1000, false);
}

Here PrintLater is the caller, SetInlineTimer is the callee, and PrintNow is the called. However, it is NOT called before SetInlineTimer returns, because we defer the call via SetTimerEx. Therefore we need some extra code to tell y_inline not to clear memory (i.e. get rid of the closure containing a, b, and c):

forward InlineTimerCall(Func:tt<>, bool:repeat);

public InlineTimerCall(Func:tt<>, bool:repeat)
{
	@.tt();
	if (!repeat)
	{
		Indirect_Release(tt);
	}
}

SetInlineTimer(Func:tt<>, delay, bool:repeat)
{
	Indirect_Claim(tt);
	SetTimerEx("InlineTimerCall", delay, repeat, "ii", _:tt, _:repeat);
}

PrintLater doesn't need to change - all of this is transparent to the end-user of a library. Indirect_Claim tells the memory to stay. Indirect_Release tells it to go.

Metadata

You can attach extra data to inline functions, for example, to write KillInlineTimer:

SetInlineTimer(Func:tt<>, delay, bool:repeat)
{
	Indirect_Claim(tt);
	new timer = SetTimerEx("InlineTimerCall", delay, repeat, "ii", _:tt, _:repeat);
	Indirect_SetMeta(tt, timer);
	return _:tt;
}

KillInlineTimer(tt)
{
	new timer = Indirect_GetMeta(tt);
	KillTimer(timer);
	Indirect_Release(tt);
}

Here the true timer id is stored along-side the inline closure data, and the handle is disguised as a timer ID.

Indirect_FromCallback

This is just a pre-defined public function:

forward Indirect_FromCallback(Func:cb<>, bool:release);

public Indirect_FromCallback(Func:cb<>, bool:release)
{
	@.cb();
	if (release)
		Indirect_Release(cb);
}

You can use it any time you need to convert a public function to an inline (e.g. to wrap an existing API):

stock BCrypt_HashInline(text[], cost, Func:cb<>)
{
	Indirect_Claim(cb);
	bcrypt_hash(text, cost, "Indirect_FromCallback", "ii", _:cb, true);
}

This is more common than you may think - closures mean that passing addition parameters to inline functions is rarely required.

String Inputs

This doesn't work:

PrintLater(string:str[])
{
	inline const PrintNow()
	{
		print(str);
	}
	SetInlineTimer(using inline PrintNow, 1000, false);
}

Any pointers parameters (references, strings, and arrays) to the caller function are not stored in the closure**. The closure copies all the local memory automatically, this remote memory must be copied manually:

PrintLater(string:str[])
{
	new localString[32];
	StrCpy(localString, str);
	inline const PrintNow()
	{
		print(localString);
	}
	SetInlineTimer(using inline PrintNow, 1000, false);
}

Backwards Compatibility

The old y_inline API - Callback_Get, Callback_Call, E_CALLBACK_DATA etc. still work, but will now give deprecation and tag mismatch warnings. The former is for the functions - all but Callback_Restore have been replaced by indirection.inc, the latter is for passing inlines since the old version didn't check that the given functions had the correct types. Also, because Callback_Get is no longer required, inlines can be used anywhere in the callee stack, not just the first called function.

Destructors

Be very careful when using tags with destructors around inline functions. Consider the following code:

operator~(Tag:data[], len)
{
	// Destructor code goes here.
}

main()
{
	new Tag:a;
	
	inline Inline()
	{
	}
	DoCall(using inline Inline);
}

The destructor for a will be called when main ends, but won't be called when Inline ends, despite the fact that it is in scope via the closure. On the other hand, an explict return will cause the destructor to be called:

main()
{
	new Tag:a;
	
	inline Inline()
	{
		return; // Destructor called.
	}
	DoCall(using inline Inline);
}

Variables with destructors declared within the inline - either as parameters or locals, are always handled correctly. It is only tagged destructors in the closure that are ambiguous (the correct behaviour isn't even obvious - should the destructor be called twice (or more, if the inline is called many times) on a single variable?)

Examples

Login Dialog

#include <YSI_Coding\y_inline>
#include <YSI_Extra\y_inline_mysql>

ShowLogin(MySQL:handle, playerid)
{
	// Called when the player responds to the dialog.
	inline const Login(pid, dialogid, response, listitem, string:inputtext[])
	{
		// `playerid` and `pid` are always the same - so we only really need
		// one of them (`pid` would be required if the inline was elsewhere).
		#pragma unused pid, dialogid, listitem
		
		// Did the user click cancel or not type anything?
		if (!response || isnull(inputtext))
		{
			// Try again.
			SendClientMessage(playerid, COLOUR_ERROR, "Login failed - please enter a password.");
			ShowLogin(handle, playerid);
			return;
		}
		
		// Called when the data is fully loaded from the database.
		inline const Loaded()
		{
			new
				count;
			if (!cache_get_row_count(count))
			{
				// There was an error loading the data.  Try again.
				SendClientMessage(playerid, COLOUR_ERROR, "Login failed - please try again.");
				ShowLogin(handle, playerid);
				return;
			}
			
			// Was the user found?
			if (count != 1)
			{
				// Try again.
				SendClientMessage(playerid, COLOUR_ERROR, "Login failed - unknown username or password.");
				ShowLogin(handle, playerid);
				return;
			}
			
			// Get the password hash and unique (user) ID.
			new
				uid,
				hash[BCRYPT_HASH_LENGTH];
			if (!cache_get_value_index(0, 0, hash) || !cache_get_value_index_int(0, 1, uid))
			{
				// There was an error loading the data.  Try again.
				SendClientMessage(playerid, COLOUR_ERROR, "Login failed - please try again.");
				ShowLogin(handle, playerid);
				return;
			}
			
			// Called when the comparison between the stored and entered
			// passwords is complete (so the login is complete).
			inline const Checked(bool:same)
			{
				// Are the passwords the same?
				if (same)
				{
					// The player logged in.  Tell everything else so they can
					// respond appropriately (start loading data etc.)
					CallRemoteFunction("OnPlayerLogin", "ii", playerid, uid);
				}
				else
				{
					// Typed the wrong password.  But never let the user know
					// the exact reason for the failure - saying `incorrect
					// password` tells them that the account exists, which may
					// be too much information.
					SendClientMessage(playerid, COLOUR_ERROR, "Login failed - unknown username or password.");
					
					// Try again.
					ShowLogin(handle, playerid);
				}
			}
			
			// Check that the DB hash is equal to `inputtext` after hashing.
			// `inputtext` is still in scope here thanks to "closures" -
			// anything defined in an outer function is available in an inner
			// inline.
			BCrypt_CheckInline(inputtext, hash, using inline Checked);
		}
		
		// Request data from the DB on this player.  Search by name.
		new name[MAX_PLAYER_NAME];
		GetPlayerName(playerid, name, sizeof (name));
		MySQL_PQueryInline(handle, using inline Loaded, "SELECT `password_hash`, `uid` FROM `users` WHERE `username` = '%e'", name);
	}
	
	// Show the login dialog box.  When it is responded to, the inline function
	// called `Login` is called.  Because inline functions follow scoping rules,
	// this must be defined BEFORE it is used (i.e. above).
	Dialog_ShowCallback(playerid, using inline Login, DIALOG_STYLE_PASSWORD, "Login", "Enter your password below", "OK", "Cancel");
}

public OnPlayerConnect(playerid)
{
	ShowLogin(gMySQL, playerid);
}

* This is due to the way inlines are implemented with macros. The compiler sees the outer function and any inline functions as one large function. Thus, the compiler thinks that some parts of the large function has returns, while other parts don't. This gives warning 209: function "NAME" should return a value.

** More strictly, the parameter itself IS stored in the closure, but the memory it points to isn't. There is no good way at run-time to determine pointer parameters, and so their destination memory isn't stored, thus the target could be no longer valid, or garbage.

BCrypt

There are two main BCrypt plugins, and YSI supports them both. If you want to use the inline MySQL functions you just need to include MySQL at some point, and the code will work. However, because YSI needs to know WHICH BCrypt plugin you're using, you need to include them first:

//
//   #include <samp_bcrypt>
//
// OR
//
//   #include <bcrypt>
//
#include <YSI_Coding\y_inline>
#include <YSI_Extra\y_inline_bcrypt>

After that, the code is exactly the same - y_inline unifies the APIs and passes you the results:

Hashing

HashPassword(const input[])
{
	inline const OnHashed(string:result[])
	{
		printf("Hashed as: %s", result);
	}
	// Use `_` instead of `12` for the default cost parameter.
	BCrypt_HashInline(input, 12, using inline OnHashed);
}

See below for an important note about string parameters.

Checking

CheckPassword(const input[], const hash[])
{
	inline const OnChecked(bool:same)
	{
		printf("Are they the same?: %s", same ? ("Yes") : ("No"));
	}
	// Use `_` instead of `12` for the default cost parameter.
	BCrypt_CheckInline(input, hash, using inline OnChecked);
}

See below for an important note about string parameters.

String Parameters

When an inline function is called it can access variables from the surrounding function. For example:

OuterFunc(a)
{
	new b;
	inline InnerFunc(c)
	{
		new d;
		printf("%d, %d, %d, %d", a, b, c, d);
	}
}

But there are exceptions - pass-by-reference parameters. Most importantly, that includes strings. Strings (param[]), arrays (param[]), references (&param), and varargs (...), are all pointers to elsewhere in memory. But when an inline function is called that memory has probably changed in the meantime to something else entirely. This means that using any of those from within an inline function might give rubbish or even crash. Sadly, there's no easy way to fix this. PawnPlus resolves this issue in async functions by making a copy of the whole of memory, which is an unacceptable performance loss IMHO, but does point to the better solution - make a copy of just the memory you need.

Instead of:

OuterFunc(string[])
{
	inline InnerFunc()
	{
		printf("%s", string);
	}
}

Simply cache the string like so:

OuterFunc(outerString[])
{
	new innerString[32];
	StrCpy(innerString, outerString);
	inline InnerFunc()
	{
		printf("%s", innerString);
	}
}

The function parameter outerString is somewhere else in memory. The local variable innerString is on the stack, within the closure stored for the inline function. This also means that you can modify innerString and the change will persist if the inline function is not const.

Note that this does NOT apply to strings passed to outer inline functions - those are already on the stack. This is fine as-is:

OuterFunc()
{
	inline InnerFunc1(string:innerString[])
	{
		inline InnerFunc2()
		{
			printf("%s", innerString);
		}
	}
}

Extras

The "extras" are functions already wrapped for use with inlines. Functions that would normally take or require a callback, but that can now be used with callbacks instead (i.e. inlines or callbacks). These, as special extras, are under YSI_Extra, and there are currently (at least) four groups:

#include <YSI_Extra\y_inline_bcrypt>
#include <YSI_Extra\y_inline_mysql>
#include <YSI_Extra\y_inline_requests>
#include <YSI_Extra\y_inline_timers>

They used to be automatically included with y_inline, but doing so bloated the code even if you didn't use them, so now you have to be more explicit. If you used them before, and don't update your code, you will now get a warning like:

error 004: function "ORM_InsertInline" is not implemented

y_inline_bcrypt

  • BCrypt_CheckInline(const text[], const hash[], Func:cb<i>);
  • BCrypt_HashInline(const text[], cost = 12, Func:cb<s>);

There are two different BCrypt plugins, these functions work with either - but you must include one of them first.

//
//   #include <samp_bcrypt>
//
// OR
//
//   #include <bcrypt>
//
#include <YSI_Coding\y_inline>
#include <YSI_Extra\y_inline_bcrypt>

ComparePassword(const input[], const hash[])
{
	inline const OnCompared(bool:match)
	{
		printf("The passwords %s match", match ? ("do") : ("do not"));
	}
	BCrypt_CheckInline(input, hash, using inline OnCompared);
}

y_inline_mysql

  • MySQL_PQueryInline(MySQL:handle, Func:cb<>, const query[], GLOBAL_TAG_TYPES:...);
  • MySQL_TQueryInline(MySQL:handle, Func:cb<>, const query[], GLOBAL_TAG_TYPES:...);
  • ORM_SelectInline(ORM:id, Func:cb<>);
  • ORM_UpdateInline(ORM:id, Func:cb<>);
  • ORM_InsertInline(ORM:id, Func:cb<>);
  • ORM_DeleteInline(ORM:id, Func:cb<>);
  • ORM_LoadInline(ORM:id, Func:cb<>);
  • ORM_SaveInline(ORM:id, Func:cb<>);

MySQL_PQueryInline and MySQL_TQueryInline both take variable parameters. These are NOT passed to the inline function with the result (that's taken care of via closures). Rather, they are formatting parameters for the query (wraps mysql_format).

y_inline_requests

  • Request:RequestCallback(RequestsClient:id, const path[], E_HTTP_METHOD:method, Func:callback<iisi>, body[] = "", Headers:headers = Headers:-1);
  • Request:RequestJSONCallback(RequestsClient:id, const path[], E_HTTP_METHOD:method, Func:callback<iii>, Node:json = Node:-1, Headers:headers = Headers:-1);

y_inline_timers

  • Timer_CreateCallback(Func:func<>, initialOrTime, timeOrCount = 0, count = -1);
  • Timer_KillCallback(func);

There are several ways to use Timer_CreateCallback function:

Timer_CreateCallback(func, 100); - Calls the function once every 100ms.

Timer_CreateCallback(func, 100, 0); - Same as above (count is 0, which means repeat forever).

Timer_CreateCallback(func, 100, 5); - Calls the function once every 100ms, but only 5 times.

Timer_CreateCallback(func, 100, 200, 0); - Calls the function once every 200ms, but the first call is after just 100ms.

Timer_CreateCallback(func, 100, 200, 42); - Calls the function 42 times, first after 100ms, then every 200ms.

In summary:

When only one parameter is given its time.

When two parameters are given they're time and count.

When all three parameters are given they're initial, time, and count.

Count is NOT like repeat in SetTimer. 0 means repeat forever, anything not 0 means call it exactly that many times.

Timer_KillCallback is used to kill a timer created by Timer_CreateCallback, since the normal KillTimer won't work for them:

inline const AfterFiveSeconds()
{
	printf("Will never be called");
}
new timer = Timer_CreateCallback(using inline AfterFiveSeconds, 5000);
Timer_KillCallback(timer);

There were two other functions, but they were quickly deprecated in favour of the open.mp-style function names above:

  • SetCallbackTimer(Func:func<>, initialOrTime, timeOrCount = 0, count = -1);
  • KillCallbackTimer(func);