286 lines
9.7 KiB
ReStructuredText
286 lines
9.7 KiB
ReStructuredText
|
|
.. _extending:
|
|
|
|
=================
|
|
Custom Extensions
|
|
=================
|
|
|
|
As shown in the :ref:`library section <library_functions>`, Spicy's
|
|
runtime library comes with a set of built-in functions that grammars
|
|
can leverage for more complex tasks, such as Base64 decoding and
|
|
``zlib`` decompression. Behind the scenes, these functions are just
|
|
small Spicy wrappers that call into corresponding C++ code. This
|
|
section describes how you can provide your own built-in functions
|
|
leveraging custom external C++ code.
|
|
|
|
Basic Example
|
|
=============
|
|
|
|
Let's start with a simple example. Say we want to make a ``rot13``
|
|
function available to Spicy that rotates each letter of a string by 13
|
|
characters, using the following C++ implementation:
|
|
|
|
.. literalinclude:: examples/mylibrary.cc
|
|
:caption: mylibrary.cc
|
|
:language: c++
|
|
|
|
We can make this function available to Spicy by declaring it inside a
|
|
custom module like this:
|
|
|
|
.. literalinclude:: examples/mylibrary.spicy
|
|
:caption: mylibrary.spicy
|
|
:language: spicy
|
|
|
|
Now we can use it in our Spicy code:
|
|
|
|
.. spicy-code:: rot13.spicy
|
|
|
|
module Example;
|
|
|
|
import MyLibrary;
|
|
|
|
const data = "Hello, world!";
|
|
global encrypted = MyLibrary::rot13(data);
|
|
global decrypted = MyLibrary::rot13(encrypted);
|
|
|
|
print "'%s' -> '%s' -> '%s'" % (data, encrypted, decrypted);
|
|
|
|
To compile and execute this, we give ``spicyc`` all three files:
|
|
|
|
.. spicy-output:: rot13.spicy
|
|
:exec: spicyc -j %INPUT programming/examples/mylibrary.spicy programming/examples/mylibrary.cc
|
|
:show-as: spicyc -j %INPUT mylibrary.spicy mylibrary.cc
|
|
|
|
Let's look more closely at what's going on here.
|
|
|
|
In ``mylibrary.spicy``, the function attribute ``&cxxname`` is the
|
|
marker for Spicy that we're not declaring a standard Spicy function
|
|
but an external function that's implemented in C++. The value of that
|
|
attribute is the C++-side name of the function, which Spicy will use
|
|
to call it. In our case, the C++ name is the same as the fully
|
|
qualified Spicy-side name, because we aligned C++ namespace and
|
|
function ID accordingly. However, that doesn't need to be the case;
|
|
see below for more.
|
|
|
|
Besides the naming, the key to interfacing Spicy with C++ lies in
|
|
aligning the types for function parameters and results between the two
|
|
sides. Internally, Spicy automatically creates a C++ function
|
|
prototype for any function declaration coming with a ``&cxxname``
|
|
attribute. To do so, Spicy maps its own types to corresponding C++
|
|
types. We can see how that looks in our example by running ``spicyc
|
|
-P <prefix>`` to print out the generated function prototype (plus a bit of
|
|
boilerplate to produce a complete C++ ``#include`` header):
|
|
|
|
.. spicy-output:: mylibrary.spicy
|
|
:exec: spicyc -P mylib programming/examples/mylibrary.spicy
|
|
:show-as: spicyc -P mylib mylibrary.spicy
|
|
|
|
As you can see, Spicy maps ``rot13``'s string argument and result into
|
|
``std::string``, which happens to be exactly what we need in our
|
|
simple example. (The prefix ``mylib`` goes into some internal IDs to
|
|
avoid nameclashes with other generated code.)
|
|
|
|
Advanced Usage
|
|
==============
|
|
|
|
In practice, it's often not quite as simple to provide new built-in
|
|
functions as in our example because parameters or results might not
|
|
directly align between Spicy and C++. In the following we walk through
|
|
aspects that typically come up here, in particular when interfacing to
|
|
already existing C++ code that doesn't know anything about Spicy.
|
|
|
|
Function naming
|
|
^^^^^^^^^^^^^^^
|
|
|
|
As said above, the name of the C++ function must be provided to Spicy
|
|
through the ``&cxxname`` attribute. The name may be namespaced, but
|
|
can also just be a global identifier, depending on what the C++ code
|
|
expects. Spicy will simply use the name literally in any C++ code it
|
|
generates.
|
|
|
|
Type mapping
|
|
^^^^^^^^^^^^
|
|
|
|
For each Spicy type, the compiler picks a corresponding C++ type when
|
|
generating an internal function prototype. The following table shows
|
|
those mappings:
|
|
|
|
.. list-table::
|
|
:widths: auto
|
|
:header-rows: 1
|
|
:align: center
|
|
|
|
* - Spicy Type
|
|
- C++ type
|
|
|
|
* - ``addr``
|
|
- ``hilti::rt::Address``
|
|
|
|
* - ``bitfield(N)``
|
|
- ``uintN_t``
|
|
|
|
* - ``bool``
|
|
- ``hilti::rt::Bool``
|
|
|
|
* - ``bytes``
|
|
- ``hilti::rt::Bytes``
|
|
|
|
* - ``enum``
|
|
- ``enum`` (a corresponding C++ enum type will be generated)
|
|
|
|
* - ``exception``
|
|
- ``hilti::rt::Exception``
|
|
|
|
* - ``(u)int8/16/32/64``
|
|
- ``hilti::rt::integer::safe<(u)int_8/16/32/64_t>``
|
|
|
|
* - ``interval``
|
|
- ``hilti::rt::Interval``
|
|
|
|
* - ``list<T>``
|
|
- ``hilti::rt::Vector<T>``
|
|
|
|
* - ``map<K,V>``
|
|
- ``hilti::rt::Map<K,V>``
|
|
|
|
* - ``optional<T>``
|
|
- ``std::optional<T>``
|
|
|
|
* - ``port``
|
|
- ``hilti::rt::Port``
|
|
|
|
* - ``real``
|
|
- ``double``
|
|
|
|
* - ``regexp``
|
|
- ``hilti::rt::RegExp``
|
|
|
|
* - ``set<T>``
|
|
- ``hilti::rt::Set<T>``
|
|
|
|
* - ``sink``
|
|
- ``spicy::rt::Sink``
|
|
|
|
* - ``stream``
|
|
- ``spicy::rt::Stream``
|
|
|
|
* - ``string``
|
|
- ``std::string``
|
|
|
|
* - ``struct``
|
|
- ``struct`` (a corresponding C++ struct type will be generated)
|
|
|
|
* - ``time``
|
|
- ``hilti::rt::Time``
|
|
|
|
* - ``tuple<Ts>``
|
|
- ``std::tuple<Ts>``
|
|
|
|
* - ``unit``
|
|
- ``struct`` (a corresponding C++ struct type will be generated)
|
|
|
|
* - ``vector<T>``
|
|
- ``hilti::rt::Vector<T>``
|
|
|
|
The C++ types that reside inside the ``hilti::rt`` or ``spicy::rt``
|
|
scopes, are defined in ``hilti/runtime/libhilti.h`` and
|
|
``spicy/runtime/libspicy.h``, respectively.
|
|
|
|
If these type mappings match what your C++ code expects---as it did in
|
|
our example---then there's nothing else to do. If they don't, you have
|
|
three options:
|
|
|
|
1. You adapt the function's C++ declaration accordingly, assuming you
|
|
can modify it.
|
|
|
|
2. If you are lucky, you may be able to get away with "slightly
|
|
mismatching" C++ types as long as (a) they coerce into each other,
|
|
and (b) Spicy is able to see the original prototype so that it can
|
|
skip generating its own. However, it may be tricky to satisfy these
|
|
two conditions, see the box below for more.
|
|
|
|
3. You provide an additional C++ wrapper function receiving the
|
|
expected types and forwarding them to the actual function as
|
|
appropriate.
|
|
|
|
.. admonition:: Using external prototypes
|
|
|
|
Per option (2) above, it is possible to get away with "slightly
|
|
mismatching" types in some cases. For example, if your C++ function
|
|
expects just an ``unsigned int`` for one of its arguments, but
|
|
Spicy passes a value as an ``uint8_t``, that will compile just
|
|
fine. But there's a caveat: to make that work, you need to prevent
|
|
Spicy from generating its own prototype for the function, as the
|
|
two would mismatch. There's a ``&have_prototype`` attribute for
|
|
that: If you add that to the Spicy-side function declaration, Spicy
|
|
will assume that it can rely on an already existing C++ prototype
|
|
instead of creating its own.
|
|
|
|
However, there's a second challenge here now: Spicy's generated C++
|
|
code needs to actually find that existing prototype somewhere—-but
|
|
unfortunately, there's currently no way of explicitly providing
|
|
it. The only case where you can make this work right now is when
|
|
Spicy's C++ runtime library happens to be already including a C++
|
|
header that comes with your desired prototype. That's unlikely for
|
|
any non-standard functionality, but it may work if you're wrapping
|
|
a standard system function, such as anything from ``stdlib.h`` for
|
|
example (e.g., ``random()``).
|
|
|
|
.. todo::
|
|
|
|
We should add a mechanism to provide an arbitrary
|
|
custom C++ prototype directly. (:issue:`1286`)
|
|
|
|
There's a similar trick for complex types, such as structs and
|
|
enums: If your C++ function requires a type that Spicy doesn't
|
|
know anything about, you can declare a Spicy-side dummy
|
|
substitute like this:
|
|
|
|
.. spicy-code::
|
|
|
|
public type MyType = __library_type("MyType");
|
|
|
|
Then you can use ``MyType`` as as type in Spicy-side declarations.
|
|
The name given to ``__library_type`` works similar to function
|
|
names provided to ``&cxxname``: the Spicy compiler will take them
|
|
literally to refer to the C++ type. However, this will work only in
|
|
similar situations as ``&have_prototype```: the compiler must be
|
|
able to find an existing declaration for that C++ type in any of
|
|
its standard includes files. It's fine for that declaration to be
|
|
just a forward declaration if that's sufficient for the C++ code to
|
|
compile).
|
|
|
|
Linking to Libraries
|
|
^^^^^^^^^^^^^^^^^^^^
|
|
|
|
In our example, we gave the custom C++ code directly to the Spicy
|
|
compiler. That code will then simply be compiled along with everything
|
|
else and be linked into the resulting binary code. Often, however, you
|
|
may instead want to make functions available to Spicy that are
|
|
implemented inside an external C++ library. In that case, Spicy will
|
|
need to link the binary code to that library. To support that,
|
|
``spicyc`` provides an option ``--cxx-link`` that takes the full path
|
|
to a (static or shared) library to link in. For example::
|
|
|
|
# spicyc -j --cxx-link /usr/local/lib/libz.a mycode.spicy
|
|
|
|
``--cxx-link`` can be specified multiple times.
|
|
|
|
Include paths
|
|
^^^^^^^^^^^^^
|
|
|
|
If your C++ code requires additional include files outside of standard
|
|
include paths, you can set the environment variable
|
|
``HILTI_CXX_INCLUDE_DIRS`` to a colon-separated list of additional
|
|
directories for ``spicyc`` to use when compiling C++ code.
|
|
|
|
Passing arbitrary compiler flags
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
If you need to set arbitrary compiler flags you can use the environment
|
|
variable ``HILTI_CXX_FLAGS``. If this value is set it will be added verbatim as
|
|
the last argument to the compiler invocation used when compiling C++ code.
|
|
|
|
.. warning:: Prefer using ``HILTI_CXX_INCLUDE_DIRS`` for setting include paths
|
|
since paths set this way will be searched before implicit paths.
|