-
Notifications
You must be signed in to change notification settings - Fork 18
AMX
AMX is a virtual machine that executes AMX assembly code contained in .amx files that were compiled from Pawn. To understand some of the functions this plugin offers, understanding of the layout of the AMX machine is useful.
The memory in an AMX machine is segmented into several sections: the code, the data, the heap, and the stack. The code segment contains low-level instructions representing basic operations the machine can do (arithmetics, variable assignment, jumps, calls etc.). The data segment contains global and static variables that were defined in Pawn, i.e. those whose lifetime lasts for the duration of the whole program. The heap and the stack are temporary segments containing intermediate values of functions. The stack contains local variables or arguments to functions, while the heap is used for dynamic memory, reference parameters, or array arguments passed to callbacks. The stack may be further segmented into stack frames created for each called function.
To run a code from an AMX machine, the host application must call it via one of its entry points. This can be a public function, the main function, or a continuation from a paused state. The code can exit from an entry point either naturally by returning from the public function, or by halting the AMX machine with an error code (by using #emit halt
or amx_yield
.
The plugin is notified when an AMX loads or unloads, and automatically manages the extra memory it needs for storing additional data bound to an AMX. This piece of metadata is called extra by the plugin and various parts of the plugin can dynamically register their own pieces of metadata. To prevent dangling pointers, shared_ptr
is used to access these metadata inside the plugin.
Other pieces of extra information can be stored in a context. A context is a temporary state created by calling a public function, like when a stack frame is created for a normal function. Since instances of the AMX machine are usually permanent, storing information in a context has the advantage of automatic cleanup when resources used by the code bound by the context need to be automatically deleted. Functions like pawn_guard
use the context as their storage, and when the context is deleted, the guards automatically free the resources as well.
Contexts are stored in a stack-like data structure for each AMX machine, since natives like CallLocalFunction can cause another public function in the same AMX to be called externally. When a public function is called normally (without a native), no new context is created.
When using multiple scripts, it may be hard to pass data from one script to another. Functions like CallRemoteFunction require parsing of parameters and finding the correct entry point in another AMX. This plugin allows to create a reference to a variable in the AMX machine's memory and pass that variable to a code in another AMX.
Such a reference can be created with amx_var
pointing to a single cell, or amx_var_arr
pointing to an array. The reference object holds both the handle to the AMX machine and the offset into its memory where the variable starts.
static var = 10;
new Var:ref = amx_var(var);
assert amx_valid(ref);
amx_set(ref, 15);
assert var == 15;
amx_delete(ref);
Every created reference must be deleted explicitly. amx_valid
returns true if the reference can be accessed, which means it will return false even if the actual reference object stills exists but the target AMX is no longer available. These references are designed for low-level access to the memory of the AMX, thus they don't employ any sophisticated tag checks like variants and other dynamic data structures do.
AMX forking is an advanced way of creating a new context or an AMX machine from the current one. The primary function for this is amx_fork
:
new result, Variant:var = var_new(0);
if(amx_fork(.result=result))
{
pawn_guard(var);
amx_yield(15);
}
assert result == 15 && !var_valid(var);
By default, amx_fork
creates a copy of the current AMX machine and runs the code that follows the function call. In the cloned AMX machine, the function returns true, and thus pawn_guard
is called, guarding the variant. Once the code exits (by returning from the entry point or by calling amx_yield
), the fork is destroyed and the code in the original AMX machine is executed from the original point, but with amx_fork
returning false there. result
is the variable which receives the value returned from the forked AMX machine. Since pawn_guard
is bound to the current context, once the forked AMX exits, the context is destroyed and the variant is deleted.
The forked context doesn't have to be destroyed immediately. In the following case, the context is saved by calling wait_ms
and restored after the given time:
new result;
if(amx_fork(.result=result))
{
task_yield(15);
wait_ms(100);
print("After 100 ms");
amx_yield(20);
}
assert result == 15;
Since pauses inside a new context don't propagate to the parent, the old code will continue when wait_ms
gets called. task_yield
can be used as the usual mechanism to return a value from a code that is about to get paused.
Using the result
variable is necessary, because the new AMX machine has completely separate memory, so attempting to assign it directly will not update the original value. However, using AMX references or variants will work:
new result;
new Var:ref = amx_var(result);
if(amx_fork())
{
amx_set(ref, 50);
amx_yield();
}
assert result == 50;
There is another way to exit the forked code. Calling amx_commit
will discard the original context and replace it with the forked one, continuing from the call to amx_commit
onwards. However, note that since the original context is destroyed, it may result in unexpected behaviour:
new Variant:v = var_new(0);
pawn_guard(v);
if(amx_fork())
{
amx_commit();
}
assert !var_valid(v);
Since v
was guarded in the original context, it is freed when the call to amx_commit
happens. This can be fixed by calling amx_commit(false)
, since that will discard the forked context instead of the original one.
Having to clone an AMX machine together with its code and data may sometimes prove to be unnecessary. In that case, amx_fork
can be used to create a protected context that protects the memory but doesn't allocate a new AMX machine:
new var = 10;
if(amx_fork(fork_data, .use_data = false))
{
var = 0;
amx_yield();
}
assert var == 10;
The first argument to amx_fork
is the level of forking, one of fork_exec
, fork_data
, and fork_machine
. fork_exec
only stores and restores the original registers of the machine, without copying any memory at all (use_data
makes no difference). fork_data
does not clone the AMX machine, but it does preserve the stack and the heap (optionally the static data as well, if use_data
is true). fork_machine
clones the entire AMX machine and executes the code there. If use_data
is false then, the static data of the machine will not be copied at all and will result in their initial values (and amx_commit
will not copy them back).
Similarly to the threaded
statement, there is amx_forked
that creates a forked block. The code in a forked block is initialized by calling amx_fork
, passing the arguments to amx_forked
, and amx_end_fork
is called automatically at the end (it checks whether the code is actually forked, in comparison to amx_yield
, but it doesn't allow returning a value).
If the code attempts to escape a forked block, a destructor is used to exit the fork. Both break
and amx_yield
can be used in a forked block to exit it, but amx_commit
shouldn't be used (even though it works, it breaks the purpose of the pseudo-statement).
new result;
amx_forked(.result=result)
{
task_yield(1);
}
assert result == 1;
Since multiple threads cannot be run in a single AMX machine, forking is a solution to creating a new independent thread. The thread will preserve the context of the fork and will keep the AMX machine alive.
new result;
amx_forked(.result=result)
{
task_yield(1);
threaded(sync_explicit)
{
thread_sleep(1000);
print("Thread end");
}
print("Fork end"); //after 1 s
}
assert result == 1; //immediately
Since the new AMX machine has independent execution, it is possible to start any number of threads executing the same code at the same time:
for(new i = 0; i < 5; i++)
{
amx_forked() threaded(sync_explicit)
{
thread_sleep(1000+i*100);
printf("Thread %d ends", i);
}
}
print("Threads are started");
Each thread will have a copy of the original memory to work with, independent on the other threads. Combined with task functions, it is possible to run a synchronous code after all the threads are finished:
amx_forked(fork_exec)
{
new Task:tasks[3];
tasks[0] = task_new(), tasks[1] = task_new(), tasks[2] = task_new();
new Task:when_all = task_all(tasks[0], tasks[1], tasks[2]);
for(new i = 0; i < 3; i++)
{
amx_forked(.use_data = false)
{
threaded(sync_explicit)
{
thread_sleep(500+i*200);
}
printf("Thread %d is done", i);
task_set_result(tasks[i], true);
}
}
task_await(when_all);
print("Threads are done");
}
print("Threads are started");