Mastodon

What Does the [NoWrapper] Attribute in Vala Mean?

Vala is one of my favorite programming languages for getting stuff done. It’s a primarily object-oriented language that looks an aweful lot like C# if you squint hard enough and, more interestingly, compiles to C using the GLib Object System (more commonly referred to as GObject). This allows for the existence of many high-level features and niceties without the performance hit of running on a virtual machine. Additionally, by virtue of using C as an intermediate representation, Vala programs can leverage the entirety of the libraries within the C ecosystem (as well as the GObject-based and even C++ with some work). Bindings to these libraries exist in the form VAPI files, which, as one could probably guess, define an API for the library in Vala, and describe to the compiler how to translate the Vala semantics into C, through the use of attributes.

Attributes are described in chapter 15 of the Vala reference manual, and the NoWrapper attribute is specifically defined in section 12, "Other attributes".

NameApplies toDescription
NoWrappermethod

As you can see, it leaves much to be desired. Having seen it frequently enough to notice it yet with such irregularity to be unable to guess as to its usage, my curiosity finally got the best of me and I decided to take a deep dive into the guts of the Vala compiler to figure out what the heck it does.

Having no better idea of where to start, I began at the root of the compiler’s repository. Recursively grepping for "NoWrapper", I was surprised at how few results came up outside of the vendored VAPI directory. The only result of real interest came up in vala/valamethod.vala:

if (get_attribute ("NoWrapper") != null && !(is_abstract || is_virtual)) {
error = true;
Report.error (source_reference, "[NoWrapper] methods must be declared abstract or virtual");
return false;
}

So, we know that the NoWrapper attribute is only relevant to abstract or virtual methods. Abstract and virtual methods in Vala are similar to their counterparts in other languages. In Vala, all abstract methods are also virtual, so let’s focus on virtual methods (or functions). Vala’s object-oriented paradigms are implemented using the aforementioned GLib Object System. According to the GObject manual, methods of a class may be or non-virtual, public virtual, or private virtual. Public virtual methods are the preferred way to add overridable methods to a class, and are defined by a common method and a virtual function in the class structure in the C header file. A virtual function is just a pointer to a function, allowing it to be “overridden” by simply assigning the pointer to the new implementation. The common method along with a base version of the virtual function are implemented in the source file. The virtual function pointer is initialized to the address of this implementation in the object’s class_init function, allowing derived classes to re-implement it as desired. Otherwise, the virtual function pointer may be left NULL, meaning it is a “pure virtual” method which must be implemented by every derived class.

That was a lot to digest. Here’s an example of a public virtual method from the GObject manual:

/* declaration in viewer-file.h. */
#define VIEWER_TYPE_FILE viewer_file_get_type ()
G_DECLARE_DERIVABLE_TYPE (ViewerFile, viewer_file, VIEWER, FILE, GObject)
struct _ViewerFileClass
{
GObjectClass parent_class;
/* stuff */
void (*open) (ViewerFile *self,
GError **error);
/* Padding to allow adding up to 12 new virtual functions without
* breaking ABI. */
gpointer padding[12];
};
void viewer_file_open (ViewerFile *self,
GError **error);
/* implementation in viewer-file.c */
void
viewer_file_open (ViewerFile *self,
GError **error)
{
ViewerFileClass *klass;
g_return_if_fail (VIEWER_IS_FILE (self));
g_return_if_fail (error == NULL || *error == NULL);
klass = VIEWER_FILE_GET_CLASS (self);
g_return_if_fail (klass->open != NULL);
klass->open (self, error);
}

Note that access to the open() method is provided through a non-virtual method of the class called viewer_file_open(). This method “wraps” the arrow operator that calls the implementation pointed to by the (*open) function pointer in the ViewerFileClass class structure, klass.

Now let’s look at the example of a private virtual method (can_memory_map(), in the following):

/* declaration in viewer-file.h. */
struct _ViewerFileClass
{
GObjectClass parent;
/* Public virtual method as before. */
void (*open) (ViewerFile *self,
GError **error);
/* Private helper function to work out whether the file can be loaded via
* memory mapped I/O, or whether it has to be read as a stream. */
gboolean (*can_memory_map) (ViewerFile *self);
/* Padding to allow adding up to 12 new virtual functions without
* breaking ABI. */
gpointer padding[12];
};
void viewer_file_open (ViewerFile *self, GError **error);

See how there’s no non-virtual method wrapping the virtual one? This is what the NoWrapper attribute tells the compiler, that there is no corresponding non-virtual method to use. We can confirm this suspicion by inspecting the compiler’s test suite.

Here’s the definition of an interface, IFoo from the test of the NoWrapper attribute, with the attribute applied to an abstract method (remember that all abstract methods are also virtual), bar(), and a concrete class Foo which implements IFoo:

interface IFoo : Object {
[NoWrapper]
public abstract int bar ();
}
class Foo : Object, IFoo {
int bar () {
return 42;
}
}

And here’s the expected implementation of bar() in the class Foo:

static gint
foo_real_bar (IFoo* base)
{
Foo * self;
gint result = 0;
self = G_TYPE_CHECK_INSTANCE_CAST (base, TYPE_FOO, Foo);
result = 42;
return result;
}

Note that foo_real_bar() is static. Its scope is limited to the source file of Foo and thus is “private” in object-oriented parlance.

Here’s a simple test of an interface IFoo, with an abstract method named foo(), and again a concrete class Foo which implements IFoo:

IFoo {
public abstract bool foo ();
}
class Foo : IFoo {
public bool IFoo.foo () {
return true;
}
public int foo () {
return 42;
}
}
void main () {
var foo = new Foo ();
assert (((IFoo) foo).foo ());
assert (foo.foo () == 42);
}

And the expected implementation of foo in the class Foo:

struct _IFooIface {
GTypeInterface parent_iface;
gboolean (*foo) (IFoo* self);
};
// ...
gboolean
ifoo_foo (IFoo* self)
{
IFooIface* _iface_;
g_return_val_if_fail (IS_IFOO (self), FALSE);
_iface_ = IFOO_GET_INTERFACE (self);
if (_iface_->foo) {
return _iface_->foo (self);
}
return FALSE;
}
// ...
static gboolean
foo_real_ifoo_foo (IFoo* base)
{
Foo * self;
gboolean result = FALSE;
self = G_TYPE_CHECK_INSTANCE_CAST (base, TYPE_FOO, Foo);
result = TRUE;
return result;
}
// ...
static void
foo_ifoo_interface_init (IFooIface * iface,
gpointer iface_data)
{
foo_ifoo_parent_iface = g_type_interface_peek_parent (iface);
iface->foo = (gboolean (*) (IFoo*)) foo_real_ifoo_foo;
}

Note here that we have a non-static (“public”), non-virtual method ifoo_foo(), which wraps the access of the virtual function foo, which points to the “private” implementation foo_real_ifoo_foo().

I’m sure that by this point, if you haven’t read the GObject manual, you’re probably asking yourself what virtual private functions are for if they aren’t actually “private” in the same sense that Vala uses? I mean, they appear in public APIs, for crying out loud! Well, according to the manual, this pattern is often used to delegate tasks to child classes:

/* this accessor function is static: it is not exported outside of this file. */
static gboolean
viewer_file_can_memory_map (ViewerFile *self)
{
return VIEWER_FILE_GET_CLASS (self)->can_memory_map (self);
}
void
viewer_file_open (ViewerFile *self,
GError **error)
{
g_return_if_fail (VIEWER_IS_FILE (self));
g_return_if_fail (error == NULL || *error == NULL);
/*
* Try to load the file using memory mapped I/O, if the implementation of the
* class determines that is possible using its private virtual method.
*/
if (viewer_file_can_memory_map (self))
{
/* Load the file using memory mapped I/O. */
}
else
{
/* Fall back to trying to load the file using streaming I/O… */
}
}

In the interest of clarity, we should probably find a way to define methods with such an attribute as protected, but then again, virtual methods can have default implementations, so maybe that isn’t necessarily always the correct thing to do. In any case, first, I have a patch to improve some documentation to submit.