3

I'm writing a language interpreter in C. I'm currently implementing a system that allows writing extension modules in C for the interpreter.

These modules are loaded into a code file like a normal module, but behind the scenes they are dynamically loaded libraries written in C. This is an approach similar to the one in the Python interpreter.

Inside C code (whether in the main interpreter or in an extension module), currently the way to instantiate a class is the following:

ObjectInstance* instance = vm_instantiate_class(class_object, arguments);

vm_instantiate_class takes care of allocating the new instance, calling its classes' initializer method on it if present, and some more bookkeeping.

ObjectInstance* vm_instantiate_class(ObjectClass* klass, ValueArray args) {
    // Allocate instance
    ObjectInstance* instance = object_instance_new(klass);

    // This goes to the instance's class, gets the requested attribute, and if it's a method,
    // the method is wrapped with a BoundMethod so it remembers it's associated instance when it's called
    ObjectBoundMethod* init_method = (ObjectBoundMethod*) object_load_attribute((Object*) instance, "@init");

    // Invoke the initializer on the instance
    vm_call_bound_method(init_method, args);

    instance->is_initialized = true;
    return instance;
}

This works great for creating instances, whether when called for user code in the main interpreter loop, and whether it's called from a C extension.

However, I'm running into a dilemma when considering how I should handle instantiating native classes.

Currently, a native class in an extension is implemented like so. We define a struct which "inherits" ObjectInstance:

typedef struct {
    ObjectInstance base;
    // ... other native fields
    int x;
    int y;
} ObjectInstanceMyClass;

When object_instance_new is called by vm_instantiate_class as seen above, it checks on the class object if it's a native or user class. If it's a native one, the class object will say how much memory actually needs to be allocated for an instance (e.g. sizeof(ObjectInstanceMyClass)). If it's a user class, sizeof(ObjectInstance) is allocated.

This allows a native instance to be visible to user code like any other instance - but it can actually carry native data inside it, outside of the data types exposed through the language.

In an extension I'm writing, there is a native class that wraps a native OS resource - suppose a file handle. Inside the extension itself, I would like to be able to instantiate it the same elegant way I instantiate a regular class:

ValueArray args = make_value_array();
value_array_write(args, <a native C file handle>);
ObjectInstanceFile* file_object = (ObjectInstanceFile*) vm_instantiate_class(file_class, args);

However - I can't do that, since <a native C file handle> isn't of a legal type in my language, and thus can't be written to a ValueArray.

A workaround I can think of is this:

// Use vm_instantiate_class to create an instance, and do actual initialization
// ad-hoc in the outside code.

ValueArray args = make_value_array(); // Empty args
ObjectInstance* instance = vm_instantiate_class(file_class, args);
ObjectInstanceFile* file_object = (ObjectInstanceFile*) instance;
file_object->handle = <a native C file handle>; // Initialize the field

This approach would work - but it's just not elegant.

I would like to be able to call vm_instantiate_class on a class which wraps a native resource (that has no equivalent in the language level), and have it work without having to further patch the instance after the call.

How is this sort of thing commonly implemented in language implementations? Specifically, I would like to know about the CPython implementation, but otherwise any language implementation example would do.

1

I'm not sure about CPython, but the solution done by the Lua interpreter (and relatives) is to have an object type corresponding to a void * + methods (Lua calls this userdata). Lua doesn't have constructors, so all they need is a make_userdata(void *, methods) function that returns, effectively, a wrapper. This could easily be adapted to your case by passing one such userdata (it doens't even need to support methods, it coukd be literally just a wrapper for a pointer) in args to vm_instantiate_class.

On the other hand, you have constructors (@init). Why not have the handle be opened in the constructor instead? Then it can set itself up however way it wants to. Everything is nicely encapsulated and, as a bonus, the object is actually instantiable from user code, which doesn't seem to be the case with your current system.

0

Look for inspiration into the source code of Glib and GObject (from GTK). Read papers about the ObjVLisp model.

In RefPerSys we are facing the same issue, which is also tightly related to garbage collection.

Assume that your target platform has all the pointers (both pointer to data and pointer to function) of the same size, with a single flat virtual address space and a Von Neumann (not Harvard) architecture. This is in 2020 true on most operating systems and processors (but not all): Linux/x86-64, Windows/x86-64, Android/ARM, etc...

Then you can cast any pointer (unsafely) from or to void*. So your file_object->handle would be (for example) a FILE* that you would appropriately cast from/to a void*. You'll have tagged unions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.