Skip to content
IllidanS4 edited this page Jun 4, 2018 · 6 revisions

What is 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 heap is usually used for storing arrays, while the stack is used for storing local variables and simple arguments to functions. 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.

PawnPlus extensions

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 instance 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.

Trans-AMX communication

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.

Forking

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))
{
    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))
{
    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.

Non-cloning forking

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(.clone = false, .use_data = false))
{
    var = 0;
    amx_yield();
}
assert var == 10;

use_data specifies whether the forked code will access the global and static data of the program. The semantics of a non-cloned fork are usually consistent with the cloned one, but there are differences. If a cloned code accesses data while use_data is false, it will get their default values instead (and amx_commit will not copy them), and if it is a non-cloned code, the data will not be protected from changes (so amx_commit doesn't need to copy them). The heap and the stack are always isolated.

amx_forked pseudo-statement

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)
{
    task_yield(1);
}
assert result == 1;

Use in threading

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)
{
    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(.clone = false, .use_data = false)
{
    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_keep(Task:task_await(when_all), false);
    print("Threads are done");
}
print("Threads are started");
Clone this wiki locally