SourcePawn Transitional Syntax

From AlliedModders Wiki
Revision as of 19:59, 18 June 2014 by BAILOPAN (talk | contribs)
Jump to: navigation, search

EXPERIMENTAL: This document is a prototype and not yet complete.

Long term, we would like to provide our users a more modern language. Pawn is showing its age, and users find manual memory management, buffers, tags, and lack of object-oriented API very frustrating. We can't provide it all at once, but we can begin to take steps in a positive direction.

SourceMod 1.7 introduces what we are calling the "Transitional API". It is built on SourcePawn's "Transitional Syntax," which is a set of language tools to make Pawn feel more modern, and to allow transitioning older APIs without breaking compatibility. Our goal is that, if and when SourcePawn can become a full-fledged modern language, the transitional API will making porting efforts very minimal.

The transitional API, so far, has the following major features and changes:

* Methodmaps - Object-oriented wrappers around existing API.
* Classes - Full object-oriented API.

Methodmaps and classes are very similar, and to the user will look almost identical. The major difference is that methodmaps are an extension of the existing API and tag system. Classes actually introduce new types that have strict behavior. All new APIs will be introduced as classes.



Methodmaps are simple: they attach methods onto a tag. For example, here is our legacy API for Handles:

native CloneHandle(Handle:handle);
native CloseHandle(Handle:handle);

This is a good example our "C-like" API. Using it generally looks something like:

new Handle:array = CreateAdtArray();
PushArrayCell(array, 4);

A Methodmap can attach functions to the Handle tag, like this:

methodmap Handle {
    public Clone = CloneHandle;
    public Close = CloseHandle;

Now, our earlier array code can start to look object-oriented:

new Handle:array = CreateAdtArray();
PushArrayCell(array, 4);


The Handle system has a "weak" hierarchy. All handles can be passed to CloseHandle, but only AdtArray handles can be passed to functions like PushArrayCell. This hierarchy is not enforced via tags (unfortunately), but instead by run-time checks. Methodmaps allow us to make individual handle types object-oriented, while also moving type-checks into the compiler.

For example, here is a transitional API for arrays:

native AdtArray:CreateAdtArray();
methodmap AdtArray < Handle {
    public PushCell = PushArrayCell;

Note that CreateAdtArray now returns AdtArray instead of Handle. Normally that would break older code, but since AdtArray inherits from Handle, there is a special rule in the type system that allows coercing an AdtArray to a Handle (but not vice versa).

Now, the API looks much more object-oriented:

new AdtArray:array = CreateAdtArray();

Inline Methods

Methodmaps can declare inline methods and accessors. Inline methods can be either natives or Pawn functions. For example:

methodmap AdtArray {
    public native PushCell(value);

This example requires that an "AdtArray.PushCell" native exists somewhere in SourceMod. It has a magic initial parameter called "this", so the signature will look something like:

native AdtArray.PushCell(AdtArray:this, value);

(Of course, this exact signature will not appear in an include file - it's the signature that the C++ implementation should expect, however.)

It's also possible to define new functions without a native:

methodmap AdtArray {
    public native PushCell(value);
    public PushCells(list[], count) {
        for (new i = 0; i < count; i++) {
Lastly, we can also define accessors. Currently only "get" accessors are available. For example,
methodmap AdtArray {
    property Size {
        public get = GetArraySize;
    property bool:Empty {
        public get() {
            return this.Size == 0;
    property Capacity {
        public native get;

The first accessor simply assigns an existing function as an accessor for "Size". The second accessor is an inline method with an implicit "this" parameter. The third accessor will bind to a native with the following name and signature:

native AdtArray.Capacity.get(AdtArray:this);

Custom Tags

Methodmaps don't have to be used with Handles. It is possible to define custom methodmaps on new or existing tags. For example:

methodmap AdminId {
    public Rights() {
        return GetAdminFlags(this);

Now, for example, it is possible to do:


Constructors and Destructors

Methodmaps can also define constructors and destructors, which is useful if they are intended to behave like actual objects. For example,

methodmap AdtArray {
    public AdtArray(blocksize = 1);
    public ~AdtArray();
    public PushCell(value);

Now AdtArrays can be used in a fully object-oriented style:

new AdtArray:array = AdtArray();
delete array;


There are a few caveats to methodmaps:

  1. CloseHandle() is not yet gone. It is required to call delete on any object that previously would have required CloseHandle().
  2. When using existing natives, the first parameter of the native must coerce to the tag of the methodmap. Tag mismatches of the "this" parameter will result in an error. Not a warning!
  3. Methodmaps can only be defined on tags. Pawn has some ways of creating actual types (like via struct or class). Methodmaps cannot be created on those types.
  4. Methodmaps do not have strong typing. For example, it is still possible to perform "illegal" casts like Float:CreateAdtArray(). This is necessary for backwards compatibility, so methodmap values can flow into natives like PrintToServer or CreateTimer.
  5. It is not possible to inherit from anything other than another previously declared methodmap.
  6. Methodmaps can only be defined over scalars - that is, the "this" parameter can never be an array. This means they cannot be used for enum-structs.


The grammar for methodmaps is:

visibility ::= "public"

methodmap ::= "methodmap" { methodmap-item* } term
methodmap-item ::=
           visibility "native" label? symbol "(" decl-args ")" term
         | visibility label?  symbol "(" decl-args ")" func-body term
         | "property" label? symbol { property-decl } term
property-func ::= "get"
property-decl ::= visibility property-impl
property-impl ::=
           "native" property-func "(" ")" term
         | property-func "(" ")" func-body term
         | property-func "=" symbol


Classes are almost identical to methodmaps, with one very important distinction: they are actual types, rather than an attachment to a tag. In fact, classes form an entirely new type system in Pawn. For example, the following is possible with methodmaps:

native Handle:CreateTimer(Float:interval, Timer:func, any:data = 0);
methodmap AdtArray {
    public AdtArray(blocksize = 1);
    public ~AdtArray();
public CheckPlayers(Handle:timer, any:data)
new AdtArray:array = AdtArray();
CreateTimer(2.0, CheckPlayers, array);

If AdtArray was created via class instead of methodmap, this code would not compile. The reason is that methodmap creates a tag that can coerce to any. However, class creates a type that coerces only to related types. Those types are AdtArray, and a special builtin type called Object.

Why does it work this way? The reason is a technical deficiency in the existing Pawn compiler. When you use "any", Pawn does not tell SourceMod what the exact types of its variables are. SourceMod just sees a bunch of bits. It doesn't know whether it's a float, an integer, an array pointer, a handle, or an object. This prevents SourceMod from implementing garbage collection* - SourceMod can't automatically free memory if it can't tell where all its objects are.

Ultimately, we want garbage collection, but it's a very big and complex task. In the meantime, it's important that the transitional API make garbage collection a future possibility. To this end, "class"-based objects do not have observable values. They cannot be passed into variadic functions or functions that would coerce them into an unrelated or non-object type - including the any type. Eventually, we will introduce a new variant of any that is safe to use with objects.

  • Well, decent garbage collection. We could implement "conservative" GC, but it's very unappealing.