Learning XS - How to create an object
Over the past year, I’ve been self-studying XS and have now decided to share my learning journey through a series of blog posts. This first post introduces the fundamentals of creating an perl object from XS.
So firstly... What is XS?
XS acts as a bridge between Perl and C, allowing you to write subroutines in C and call them from Perl just like regular Perl functions. By offloading critical code to C, you can achieve significant performance improvements. XS also makes it possible to interface Perl with existing C libraries.
Next.. What is an perl Object?
A Perl object is simply a reference (usually to a hash, array, or scalar) that has been "blessed" into a package (class). This blessing associates the reference with a class, allowing you to call methods on it. Many of you will have written the following code or used one of perls many OO implementations like Moo. But what we are going to recreate today is something like the following:
You would then call this object like so:
First we will need to create a new perl distribution, I usually do this using Module::Starter, which can be found on metacpan https://metacpan.org/pod/Module::Starter.
This will create a new directory called My-Object with the basic structure of a perl module.
Next we will need to update the MakeFile.PL so that it knows we are using XS. One simple way to do this is to add XSMULTI => 1 to the WriteMakefile call. Which will tell MakeMaker to look for XS files in the lib/My/Object/ directory.
Next lets update the lib/My/Object.pm file to include the XS file we will create after.
Now we can create the XS file that will implement our object. Create a new file called lib/My/Object.xs and add the following code:
That defines the package and if we were to compile this now, it would create a lib/My/Object.so file that can be loaded into Perl and the basic tests added by Module::Starter will pass. to test that:
With that working, lets quickly write an additional test to gradually test the behaviour of our new module.
Now when we "make test" this test will fail as we have not yet added the new method that will create our object. Continuing after the PROTOTYPES line, we will add the following code: (Add a empty line after the PROTOTYPES line).
So what exactly are we doing here, if we go line by line:
1. 'SV * new(pkg, ...)': This defines a new function 'new' that takes a package name (class) as its first argument and has ... so that in the future we can handle additional optional arguments.
2. 'SV * pkg': This declares the first argument 'pkg' as a scalar value (SV) that will hold the package name.
3. 'CODE:': This indicates the start of the C code that will be executed when the 'new' method is called.
4. 'HV * hash = newHV();': This creates a new hash (hash value) that will be used to store the objects attributes.
5. 'RETVAL = sv_bless(newRV_noinc((SV*)hash), gv_stashsv(pkg, 0));': This line does two things:
- It creates a new reference to the hash using 'newRV_noinc()'.
- It then blesses this reference into the specified package (class) using 'sv_bless()', which associates the reference with the package name provided in 'pkg'. The gv_stashsv(pkg, 0)' function retrieves the stash (symbol table) for the given package name, allowing Perl to recognise the reference as an object of that class.
6. 'OUTPUT:': define the output of the function, which is the return value of the 'new' method.
7. 'RETVAL': This is the variable that will hold the return value of the 'new' method, which is the blessed reference to the new hash.
Now we can run 'make test' again and we should see that the test passes, as we have now created the 'new' method that creates a new object and blesses it into the 'My::Object' class.
Next we will need to extend so that we can also pass in a hash reference to the 'new' method. First lets add a new test:
Then update your new method to the following:
So what has changed, we now check items to see if there is a second argument passed to the 'new' method. If there is, we check if it is a reference to a hash (using 'SvROK' and 'SvTYPE'). If it is not, we 'croak' with an error message. If it is a valid hash reference, we increment its reference count using 'SvREFCNT_inc'. If no second argument is provided, we. create a new hash as before.
Again if you run 'make test' you should see that the tests pass, and we can now create an object with a hash reference passed to the 'new' method.
Next lets add tests for our 'get' and 'set' methods. Add the following tests to your test file:
Now we can implement the 'get' and 'set' methods in our XS file. Lets start with 'get' add the following code to your lib/My/Object.xs file:
What we have done here is defined a new method 'get' that takes two arguments: 'self' (the object) and 'key' (the key to retrieve from the hash). We then cast 'self' to an 'HV*' (hash value) and use 'hv_exists' to check if the key exists in the hash. If it does, we fetch the value using 'hv_fetch', increment its reference count with 'SvREFCNT_inc', and return it. If the key does not exist, we return an undefined value ('PL_sv_undef').
Next, we will implement the 'set' method. Add the following code to your lib/My/Object.xs file:
In this method, we define 'set' that takes three arguments: 'self', 'key', and 'value'. We again cast 'self' to an 'HV*' and use 'SvPV' to get the key as a string. We then increment the reference count of 'value' using 'SvREFCNT_inc', store the value in the hash using 'hv_store', and return the value.
Now we can run 'make test' again and we should see that all tests pass, and we have successfully created a Perl object from XS that matches the perl implementation.
This concludes the first part of learning XS. We have covered the basics of creating a Perl object from XS, including defining methods for object instantiation, attribute retrieval, and modification. In the next part of this series, we will dive deeper into type checking for scalar values (SVs) in XS, exploring how to ensure that the data passed to our methods is of the expected type.
Leave a comment