If you want to export a new C++ method for a class that has already been exported.
Suppose we wanted to add the method seal::Plaintext seal::AbstractIntegerEncoder::encode (std::int32_t)
that is declared in external/SEAL/src/seal/encoder.h
In this case, the class (seal::AbstractIntegerEncoder
) to which the method
belongs, and the argument type (std::int32_t
) and the result type
(seal::Plaintext
) are all already have C counterparts and are also exposed in C#.
In that case we do the following:
-
Add a new method declaration to
seal-c/include/seal-c/include/seal-c/encoder.h
The method name will be
SEAL_AbstractIntegerEncoder_encode_int32
(normally the name can just beSEAL_<className>_<methodName>
but since there are multipleencode
methods taking different arguments inAbstractIntegerEncoder
, we append_int32
to disambiguate it fromSEAL_AbstrctIntegerEncoder_encode_int64
)The method will use the opaque C struct refs from
seal-c/include/seal-c/types.h
:-
The result type
seal::Plaintext
becomesSEALPlaintextRef
(Which is just defined asstruct SEALOpaquePlaintext*
that is, a pointer to a C struct calledSEALOpaquePlaintext
. This opaque struct is not defined anywhere - it only exists to introduce a way to pass the C++ classseal::Plaintext
back and forth through the C binding library.) -
The
this
argument,seal::AbstractIntegerEncoder
is passed as aSEALAbstractIntegerEncoderRef
. -
The
int32_t
integer is just passed as is. -
Make sure the method declaration is enclosed by the pair of
BEGIN_SEAL_C_DECL
/END_SEAL_C_DECL
macros in the header - that establishes that this function will be exported with C-style naming, even though (as we will see next) it's implemented in C++.
-
-
Add the implementation of the new method to
seal-c/encoder.cpp
The implementation has to do four things:
- Unwrap all the wrapped arguments to get pointers to C++ classes instead of pointers to opaque C structs.
- Call the seal library C++ methods.
- Allocate new dynamic memory for the result C++ classes (most of the SEAL library methods return results on the stack, which doesn't interoperate well with the C# marshalling).
- Wrap the pointer to the result C++ class to make a pointer to an opaque C struct.
SEALPlaintextRef SEAL_AbstractIntegerEncoder_encode_int32 (SEALAbstrctIntegerEncoderRef encoder, int32_t i) { seal::AbstractIntegerEncoder* encoder_ptr = seal_c::wrap::unwrap (encoder); // 1 seal::Plaintext result_value = encoder_ptr->encode (i); // 2 seal::Plaintext *result_ptr = new seal::Plaintext (result_value); // 3 SEALPlaintextRef result = seal_c::wrap::wrap (result_ptr); // 4 return result; }
Each of the lines in the function above performs the corresponding step.
We can make the implementation above a bit less repetitive by not making intermediate variables for the unwrapped values. We can also use
auto
to avoid having to specify some variable's types.Finally, we use
std::make_unique
to dynamically allocate the memory for us into astd::unique_ptr<>
(This isn't strictly necesssary for this short function, but it is useful in more involved examples so that we don't leak memory in case there are errors). In this case it is important also to call therelease
method on theunique_ptr
when we return the object from this function. (If we did not, theunique_ptr
would delete the object when the function returns)SEALPlaintextRef SEAL_AbstractIntegerEncoder_encode_int32 (SEALAbstrctIntegerEncoderRef encoder, int32_t i) { seal::Plaintext result_value = seal_c::wrap::unwrap (encoder)->encode (i); // 1 and 2 auto result_ptr = std::make_unique <seal::Plaintext> (result_value); // 3, using a unique_ptr<Plaintext> return seal_c::wrap::wrap (result_ptr.release ()); // 4 and release() and return }
-
At this point we've exported the new function from the C library. We can try building
./seal-c.sh
This will update
build/install/lib/libseal-c.so
(libseal-c.dylib
on macOS) to export our new function. -
Now we can add the method to the
SEAL.Internal.AbstractIntegerEncoder
C# class insealsharp/SEAL/Internal/AbstractIntegerEncoder.cs
First we import the method from the shared library:
[DllImport (SEALC.Lib)] private static extern Plaintext SEAL_AbstractIntegerEncoder_encode_int32 (AbstractIntegerEncoder encoder, int i);
(by default the name of the C# method will be used as the name to look up in the C shared library. The
DllImport
attribute has properties to override the name if necessary. But it shouldn't be necessary.)The way the C# marshalling works is that any class that derives from
SafeHandle
(in this caseSEAL.Internal.Plaintext
andSEAL.Internal.AbstractIntegerEncoder
) will be marshalled between C# and C as a pointer to an opaque C struct. Exactly what we have. Theint
is marshalled as aint32_t
- C# integers are always 32 bits.Next we add a non-
static
public
method with a nicer name that calls the C methodpublic Plaintext EncodeLong (long l) { return SEAL_AbstractIntegerEncoder_encode_int32 (this, l); }
Note that we pass
this
for the first argument, sinceSEAL.Internal.AbstractIntegerEncoder
is aSafeHandle
this will pass a pointer to the an opaque C struct to the C method. (The actual pointer value is inSafeHandle.handle
, and the C# marshalling knows that that's the value to pass to the C code. You should only ever refer to it explicitly when writing adestroy
method - more on that in the next section about wrapping a new class.) -
Finally, we need to expose the new internal
SEAL.Internal.AbstractIntegerEncoder
method via a new public method inSEAL.AbstractIntegerEncoder
method insealsharp/SEAL/AbstractIntegerEncoder.cs
public Plaintext Encode (int i) { return new Plaintext (Handle.EncodeInt (i)); }
Things to note here:
SEAL.Plaintext
is created fromSEAL.Internal.Plaintext
using itsPlaintext (Internal.Plaintext internalPlaintext)
constructor. Every non-abstract publicSEAL
classFoo
has an internal constructor that takes aInternal.Foo
handle as an argument.We use the
Handle
property to get theSEAL.Internal.AbstractIntegerEncoder
object of the currentSEAL.AbstractIntegerEncoder
. Every publicSEAL
class has ahandle
field orHandle
property. (The reason some classes have a property is when we need to represent a C++ class hierarchy in a C# class hierarchy. We store ahandle
field in the non-abstract leaf classes and a virtualHandle
property in the abstract parent classes)The public method just delegates to the method on the internal handle object.
The public method is named
Encode (int i)
rather thanEncodeInt
for stylistic reasons - people in C# prefer to have methods the same name taking various arguments, rather than different methods with different names for each type of argument. (The internal class could have also done this, but it seemed better to stay a bit in sync with the C code). ) -
At this point we can build the C# library. (Either in visual studio, or using
msbuild
from the command line). -
Next we can try calling the method in
sealsharp-example/Main.cs
int i = 17; Plaintext encoded_int = encoder.Encode (i);
This should run and encode the integer. If we also had the corresponding decoding function implemented, we could check that encoding and decoding both work.
Suppose we want to add the C++ class seal::Evaluator
from
external/SEAL/src/seal/evaluator.h
as a new class SEAL.Evaluator
in C#.
-
First we need to make an opaque C struct for the class. We'll call it
SEALEvaluatorRef
.In
seal-c/include/seal-c/types.h
add:typedef struct SEALOpaqueEvaluator *SEALEvaluatorRef;
The C struct
SEALOpaqueEvaluator
will be a struct with no definition - it only exists so that we can wrapseal::Evalutor *
(a pointer to a C++ class) as aSEALEvaluatorRef
when we need to pass arguments or return values of that type. -
Next we need to teach the wrapper machinery about the new type.
Create a new file
seal-c/evaluator.hpp
with this boilerplate:#ifndef _SEAL_C_EVALUATOR_HPP #define _SEAL_C_EVALUATOR_HPP #include <seal/evaluator.h> #include <seal-c/types.h> #include "wrap.hpp" namespace seal_c { namespace wrap { template<> struct Wrap<seal::Evaluator*> : public WrapPair<seal::Evaluator*, SEALEvaluatorRef> {}; template<> struct Unwrap<SEALEvaluatorRef> : public WrapPair<seal::Evaluator*, SEALEvaluatorRef> {}; } // namespace wrap } // namespace seal_c #endif
This will make it so that
seal_c::wrap::wrap
can take aseal::Evaluator*
and turn it into aSEALEvaluatorRef
, andseal_c::wrap::unwrap
can turn aSEALEvaluatorRef
back into aseal::Evaluator*
.(A common source of compiler errors in laters steps is forgetting to
#include "evaluator.hpp"
in which case you will get a horrible mess ofclang
errors when you usewrap
orunwrap
) -
Next we create a new C header that will be used to declare the C wrapper functions for all the methods of
SEALEvaluatorRef
. At the beginning we will add the single most important methodSEAL_Evaluator_destroy
which will be used to call the destructor.In
seal-c/include/seal-c/evaluator.h
define:#ifndef _SEAL_C_EVALUATOR_H #define _SEAL_C_EVALUATOR_H #include <seal-c/c-decl.h> #include <seal-c/types.h> BEGIN_SEAL_C_DECL void SEAL_Evaluator_destroy (SEALEvaluatorRef evaluator); END_SEAL_C_DECL #endif
(Note that the
include/
header file ends in.h
, while theseal-c/evaluator.hpp
internal header ends in.hpp
- this is just a convention, but it's being used to distinguish the headers that will form the C API, from the headers that have C++ details that are internal to the seal-c binding.) -
Next in
seal-c/evaluator.cpp
we will implementSEAL_Evaluator_destroy
#include <seal/evaluator.h> // 1 #include <seal-c/evaluator.h> // 2 #include "evaluator.hpp" // 3 #include "wrap.hpp" // 4 void SEAL_Evaluator_destroy (SEALEvaluatorRef evaluator) { delete seal_c::wrap::unwrap (evaluator); // 5 }
The implementation is:
- We include the header that defines the C++
seal::Evaluator class
- We also include the header that declares the C
SEAL_Evaluator_destroy
function - And also the header that defines how to
wrap
andunwrap
an evaluator - And also the header that defines the
wrap
/unwrap
machinery in general - Finally in the implementation we call
delete
on theSEAL::Evaluator*
that we get by unwrapping theSEALEvaluatorRef
argument that we will get from the C# world.
- We include the header that defines the C++
-
Since
seal-c/evaluator.cpp
is a new source file, we have to add it to the theseal-c/CMakeLists.txt
file:# C++ sources target_sources (seal-c PRIVATE coeff_modulus.cpp encryption_parameters.cpp context.cpp key_generator.cpp small_modulus.cpp encoder.cpp public_key.cpp secret_key.cpp encryptor.cpp decryptor.cpp plaintext.cpp ciphertext.cpp evaluator.cpp )
Note the last line adds
evaluator.cpp
to thetarget_sources
. -
At this point we should be able to build the C library
./seal-c.sh
-
Next we add a new internal C# class derived from
SafeHandle
that will represent aSEALEvaluatorRef
in C#.In
sealsharp/SEAL/Internal/Evaluator.cs
:using System; using System.Runtime.InteropServices; namespace SEAL.Internal { class Evaluator : SafeHandle { /* called by P/Invoke when returning a Evaluator */ private Evaluator () : base (IntPtr.Zero, true) {} public override bool IsInvalid { get { return handle == IntPtr.Zero; } } protected override bool ReleaseHandle () { SEAL_Evaluator_destroy (handle); return true; } [DllImport (SEALC.Lib)] private static extern void SEAL_Evaluator_destroy (IntPtr handle); } }
Everything here is boilerplate. But the important details are:
-
The class
Evaluator
derives fromSafeHandle
. If the C++ classseal::Evaluator
had a base class we would instead derive theSEAL.Internal.Evaluator
class from the base class internal C# class. (for exampleseal::IntegerEncoder
has the base classseal::AbstractIntegerEncoder
and soSEAL.Internal.IntegerEncoder
is derived fromSEAL.Internal.AbstractIntegerEncoder
instead of fromSafeHandle
). -
We override
IsInvalid
andReleaseHandle
. TheIsInvalid
override is boilerplate - a null pointer is invalid. TheReleaseHandle
override is calling our new destroy function which is imported usingDllImport
, at the end of the file. -
We declared a
private
constructorEvaluator()
that takes no arguments. In general, theSEAL.Internal
classes don't have any other constructors. The private constructor is used by the marshalling machinery in .NET to createSEAL.Internal.Evaluator
instances whenever we willDllImport
functions with anEvaluator
as the return type.
-
-
Add a public C# class
SEAL.Evaluator
to represent evaluator instances.In
sealsharp/SEAL/Evaluator.cs
:using System; namespace SEAL { public class Evaluator { internal Internal.Evaluator handle; internal Evaluator (Internal.Evaluator h) { handle = h; } } }
This, again, is all boilerplate. The important points are:
-
If
seal::Evaluator
had a baseclass, then we would again makeSEAL.Evaluator
derive from the base class. (For example:SEAL.IntegerEncoder
derives fromSEAL.AbstractIntegerEncoder
). -
We add a new internal constructor
Evaluator (Internal.Evaluator h)
that makes aSEAL.Evaluator
from aSEAL.Internal.Evaluator
. This will be used whenever we add new public methods that need to return anEvaluator
instance. (See how we created aPlaintext
from anInternal.Plaintext
in the "adding a method" example). -
The internal handle is stored in a field with
internal
visibility. This is used to access theSEAL.Internal.Evaluator
when we implement methods that take aSEAL.Evaluator
as an argument.For classes that are base classes (e.g.
SEAL.AbstractIntegerEncoder
) we would instead have ainternal abstract
Handle
property getter that we would override in the derived class (ieSEAL.IntegerEncoder
). There should only be ahandle
field in the classes that don't themselves have any subclasses - if a class is a base class, it should just have anabstract
Handle
property.
-
-
At this point we should be able to build the SEAL library either using Visual Studio or
msbuild
from the command line.However since we didn't add any constructors there aren't examples we can run since we don't actually have any instances created.
Continuing the previous example we want to add the constructor for
seal::Evaluator
which is declared in external/SEAL/src/seal/evaluator.h
as
Evaluator(std::shared_ptr<seal::SEALContext> context)
.
This is actually similar to adding a new method to an existing class (ie we
will add a new declaration to seal-c/include/seal-c/evaluator.h
and an
implementation to seal-c/evaluator.cpp
followed by new methods in
SEAL.Internal.Evaluator
and SEAL.Evaluator
).
The points of note are:
-
std::shared_ptr<seal::SEALContext>
is represented in C with aSEALSharedContextRef
which actually wraps aseal_c::SEALSharedContext
object defined inseal-c/shared_context.hpp
- not astd::shared_ptr<seal::SEALContext>
directly. (This is because there's no easy way to work with astd::shared_ptr<>
from C, so we wrap it in a class and then use our usual opaque C struct trick to work with the class). There is otherwise no difference. -
By convention we will call the C function for this constructor
SEAL_Evaluator_construct
in
seal-c/include/seal-c/evaluator.h
addSEALEvaluatorRef SEAL_Evaluator_construct (SEALSharedContextRef context);
and in
seal-c/evaluator.cpp
addSEALEvaluatorRef SEAL_Evaluator_construct (SEALSharedContextRef context) { auto p = std::make_unique<seal::Evaluator> (seal_c::wrap::unwrap (context)->get_context ()); return seal_c::wrap::wrap (p.release ()); }
-
In
SEAL.Internal.Evaluator
we will add aninternal
static
method calledCreate
:public static Evaluator Create (SEALSharedContext context) { return SEAL_Evaluator_construct (context); }
and the usual
DllImport
[DllImport (SEALC.Lib)] private static extern Evaluator SEAL_Evaluator_construct (SEALSharedContext context);
-
In
SEAL.Evaluator
we will add a new constructorEvaluator (SEALContext)
public Evaluator (SEALContext context) { handle = Internal.Evaluator.Create (context.handle); }
As usual we compile the C library with ./seal-c.sh
and then msbuild
to build the C# library.
We can try to create a new Evaluator
in the example with:
var evaluator = new Evaluator (context);
And that will call our SEAL_Evaluator_construct
method, followed, soon after,
by SEAL_Evaluator_destroy
when the evaluator object is garbage collected.the destructor for the
class as a new method.