The stupid question is the question not asked | |
PerlMonks |
Wrapping a C shared library with Perl and XSby stevieb (Canon) |
on Mar 17, 2017 at 18:23 UTC ( [id://1185070]=CUFP: print w/replies, xml ) | Need Help?? |
So, I've been asked by a couple of people now if I would take some of the experience I've gained over the last half year or so, and put together some form of tutorial on wrapping a C library, and more generally, XS. This is the first cut of that tutorial. Relatively, I am still very new to all of this, as it's a pretty complex world. Before I started, I didn't have any real C experience, so I've been dealing with that learning curve at the same time, so I know there are better and more efficient ways of doing what I do, and would appreciate any feedback. I'll get right to it. Here's an overview:
The actual C code is irrelevant at this point, but knowing the definitions in use is, so here they are for the xswrap library:
Create a new shell distribution I use Module::Starter:
Now change into the new XS-Wrap directory, which is the root directory of the new dist. The Perl module file is located at lib/XS/Wrap.pm. I've removed a bunch of stuff for brevity, but the shell looks something like this:
Create the base XS file I use Inline::C to do this for me, as like most Perl hackers, I'm often lazy. Note the flags in use. clean_after_build tells Inline to not clean up the build directory (_Inline after build). This allows us to fetch our new .xs file. name is the name of the module we're creating this XS file for.
The resulting XS file is located in _Inline/build/XS/Wrap/Wrap.xs. Copy it to the root directory of the dist: cp _Inline/build/XS/Wrap/Wrap.xs .Here's what our base XS file looks like. It doesn't do anything yet, but we'll get there:
See the PACKAGE = main there? Change main to the name of our dist, XS::Wrap. Adding the functions from the shared library to XS Now we need to define our C functions within the XS file. After I've done that using the C definitions for the functions above, my XS file now looks like this
Note that at this point, because we're not using Inline anymore, you can remove the include for the INLINE.h header file. However, in our case, we're going to be using some Inline functionality a bit later on, so instead of removing that, copy the INLINE.h file to the same directory we copied our XS file into: cp _Inline/build/XS/Wrap/INLINE.h . Readying the module file for use Now we have some work to do to pull in the XS, wrap the functions, and export them. Note that you do not *need* to wrap the functions with Perl, you can export them directly as depicted in the XS file if you wish, as long as you know you don't need to add any further validation or functionality before the XS imported C function is called. I'll wrap all three. The functions that each wrapped function calls is the literal C function, as advertised through the XS file we hacked above.
Telling the Makefile to load the external C library Because we're using an external shared library, we need to add a directive to the Makefile.PL file. Put the following line anywhere in the Makefile.PL's WriteMakefile() routine:
Building, installing and initial test Let's build, install and write a test script for our new distribution.
At this point, if everything works as expected, you're pretty well done. However, in the case here, we're going to unexpectedly run into some issues, and we'll need to do other things before we finalize our distribution. Test script (example.pl). Very basic, it just tests all three wrapped functions:
Output:
Hmmm, something is not right. The arr() C function was supposed to return an array of three elements, 0, 1, 2, but we get no output. This is because arr() returns an unsigned char* which we can't handle correctly/directly in Perl. In this case, I will just wrap the arr() function with a new C function (I've called it simply _arr()) that returns a real Perl array based on the output from the original C arr() function. Technically, I won't be returning anything, I'm going to just use functionality from Inline to push the list onto the stack (one element at a time), where Perl will automatically pluck it back off of the stack. To do this, I'll be leveraging Inline again, but with a couple of changes. We change the name, and add also bring in our shared library because we need it directly now. Returning a Perl array from a C function
After I execute that Perl script, I'm left with a new XS file within the _Inline/build/Test/Test.xs.. It looks like this:
We only need a couple of pieces of it, so get out your CTRL-V and CTRL-C. Here are the sections (cleaned up a bit for brevity) that we need to copy into our real Wrap.xs file. The C portion:
The XS portion:
The C part goes near the top of the XS file, and the XS part goes in the XS section at the bottom. Here's our full XS file after I've merged in these changes. Finalized XS file
So, in our XS, we have four functions. Three that are imported directly from the C shared lib (mult(), speak() and arr()) and one new one written in C locally that wraps an imported XS function (_arr()). We need to do a quick update to the wrapper in the module file. Change the call to arr() to _arr() in the .pm file within the my_arr() function:
Repeat the build/install steps, then test again:
Cool! Our custom C wrapper for arr() works exactly how we want it to. We're ready for release!Creating a release of our distribution It's very trivial to do:
Of course, you have written all of your POD and unit tests before reaching this point, but I digress :) I've also posted this at blogs.perl.org. update: I want to thank all of the Monks here who have provided me help, feedback, advice and in a couple of cases, some ego-kicking. I will not name said Monks because I'm very afraid of leaving someone out, but you know who you are.
Back to
Cool Uses for Perl
|
|