What is a Delegate Approach?
It's an alternative approach to DllImport attribute when importing a function and the main advantage of this approach is the ability to access symbols other than Function Pointers in Native Library.Ironically, you'll still have to use DllImport, but only for specific tool that enables you to use the Delegate Approach with the use of DL Library.
Here the Gist for DLSupport which is a helper class for Delegate Approach.
Let's Set It Up!
Boot up your Monodevelop IDE and create a new console project and name it "Tutorial1" and place it in any directory you want.
Right click on your project "Tutorial 1" on your solution explorer, Add, New File...
A new file dialog will show up, click on General and click on Class and then type in "DLSupport" for your class name.
Copy the source code from DLSupport gist here into your new DLSupport.cs file or copy the code below:
using System;
using System.IO;
using System.Runtime.InteropServices;
public abstract class DLSupport
{
[DllImport("libdl.so.2")]
static extern IntPtr dlopen(string filename, int flags);
[DllImport("libdl.so.2")]
static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libdl.so.2")]
static extern int dlclose(IntPtr handle);
protected IntPtr handle;
public DLSupport(string dir, string lib, LoadFlag flag)
{
var path = Path.Combine(dir, lib);
if (!File.Exists(path))
throw new FileNotFoundException("Library could not be found!");
handle = dlopen(path, (int)flag);
if (handle == IntPtr.Zero)
throw new DllNotFoundException("Library cannot be loaded with given flag.");
}
public DLSupport(string lib, LoadFlag flag)
{
handle = dlopen(lib, (int)flag);
if (handle == IntPtr.Zero)
throw new DllNotFoundException("Library cannot be found or loaded with given flag.");
}
protected T GetFunctionDelegate<T>(string symbol, bool SuppressSymbolError = false)
{
var ptr = dlsym(handle, symbol);
if (!SuppressSymbolError && ptr == IntPtr.Zero)
throw new Exception(string.Format("Symbol cannot be found for type: {0}", typeof(T).Name));
return Marshal.GetDelegateForFunctionPointer<T>(ptr);
}
protected IntPtr GetSymbolPointer(string symbol)
{
return dlsym(handle, symbol);
}
/// <summary>
/// Should not be invoked unless none of DllImport uses the same library as this handle.
/// The runtime will fail if you attempt to use DllImported functions that happens to be using the same
/// handle as this.
/// </summary>
protected void DangerousDispose()
{
dlclose(handle);
}
}
public enum LoadFlag : int
{
RTLD_LAZY = 0x00001,
/// <summary>
/// Immediate function call binding.
/// </summary>
RTLD_NOW = 0x00002,
/// <summary>
/// Mask of binding time value.
/// </summary>
RTLD_BINDING_MASK = 0x3,
/// <summary>
/// Do not load the object.
/// </summary>
RTLD_NOLOAD = 0x00004,
/// <summary>
/// Use deep binding.
/// </summary>
RTLD_DEEPBIND = 0x00008,
/// <summary>
/// If the following bit is set in the MODE argument to `dlopen',
/// the symbols of the loaded object and its dependencies are made
/// visible as if the object were linked directly into the program.
/// </summary>
RTLD_GLOBAL = 0x00100,
/// <summary>
/// Unix98 demands the following flag which is the inverse to RTLD_GLOBAL.
/// The implementation does this by default and so we can define the
/// value to zero.
/// </summary>
RTLD_LOCAL = 0,
/// <summary>
/// Do not delete object when closed.
/// </summary>
RTLD_NODELETE = 0x01000
}
Your project should look like this:
Good! Now we're going to be working with C language and try to access that from C# using various approaches like DllImport and the DLSupport (Delegate Approach) which will also be covered in Tutorial #2.
So let's create a C File for our project. Right click your project, Add, New Files... and then select Misc category and then select Empty Text File and enter "Tutorial.c" for file name.
It should opens up Tutorial.c file and now we should enter the simpliest C code to demostrate the difference between two approaches. Copy the paste the code below for Tutorial.c
#include <stdint.h>
int32_t VariableInC = 1;
void IncrementVariableInC()
{
++VariableInC;
}
Now you should have something that looks like this:
The next step is to simplify the build process, so we wouldn't have to compile the code from terminal over and over. Right click on your project and then select options.
Now select Custom Commands under Build category in a the Options dialog.
Now you have two text boxes that contains the command and the working directory.
Insert the following into command text box:
gcc -shared -fPIC -o${TargetDir}/libtutorial.so Tutorial.c
And then insert the following for Working Directory text box:
${ProjectDir}
Now build your project and check if there is any error and if there are any, be sure to go back above and correct any mistakes, if the error persists, contact me and I'll try to help and update here.
The Delicate Delegate Approach
Open your Program.cs, let's rename our main class to "LibTutorial" and have it extend from DLSupport. DLSupport requires that you supply a constructor for that class that also initializes the DLSupport as well, so you have to provde the name of library and the directory where library will be located in. For now, we'll use the directory where the executable/library is located in.
You can copy and paste the code below into your Program.cs.
using System;
using System.IO;
namespace Tutorial1
{
public class LibTutorial : DLSupport
{
public LibTutorial() : base(Path.GetDirectoryName(typeof(LibTutorial).Assembly.Location),
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
}
static void Main()
{
LibTutorial lib = new LibTutorial();
}
}
}
using System.IO;
namespace Tutorial1
{
public class LibTutorial : DLSupport
{
public LibTutorial() : base(Path.GetDirectoryName(typeof(LibTutorial).Assembly.Location),
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
}
static void Main()
{
LibTutorial lib = new LibTutorial();
}
}
}
Build and run the project, if no exception being thrown, it means that the library is loaded successfully, if not, that means the library is either not compiled or the flag haven't been set right for it. Please go back up and correct any mistakes, if not, please contact me.
Let's access our variable, "VariableInC", but how exactly can you do that in C#? This is where the delegate approach comes in, you get to avoid writing a bunch of wrapper C codes to accomplish the same thing on Linux.
Let's create a property that holds a pointer for our variable.
IntPtr VariableInCPtr { get; set; }
And then assign it in the LibTutorial constructor with DLSupport helper funciton, "GetSymbolPointer" which allows us to retrieve the pointer to VariableInC address...
public LibTutorial() : base(Path.GetDirectoryName(typeof(LibTutorial).Assembly.Location),
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
VariableInCPtr = GetSymbolPointer("VariableInC");
}
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
VariableInCPtr = GetSymbolPointer("VariableInC");
}
Now we need to create a property that allows us to interact with our variable!
int VariableInC
{
get
{
}
set
{
}
}
{
get
{
}
set
{
}
}
We know that the VariableInC is a signed integer at around 32 bits long, so we need to read integer from the pointer that is being assigned at constructor. This is where Marshal.ReadInt32 come in from System.Runtime.InteropServices namespace.
Let's add
using System.Runtime.InteropServices;
along with our other using directives.
And we also have to make sure we aren't reading from a NULL pointer or a pointer that haven't been set yet, so we'll need to compare our variable pointer to IntPtr.Zero and throw an exception if there is any attempt being made to use that pointer.
For our getter, it'll be like so:
get
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
return Marshal.ReadInt32(VariableInCPtr);
}
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
return Marshal.ReadInt32(VariableInCPtr);
}
And the setter is just as simple, but we'll be using Marshal.WriteInt32 instead like so:
set
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
Marshal.WriteInt32(VariableInCPtr, value);
}
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
Marshal.WriteInt32(VariableInCPtr, value);
}
There you have it, a rather verbose property for our variable in C!
Let give it a try in reading it by accessing that variable in Main method.
Here the source code we have so far:
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Tutorial1
{
public class LibTutorial : DLSupport
{
IntPtr VariableInCPtr { get; set; }
int VariableInC
{
get
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
return Marshal.ReadInt32(VariableInCPtr);
}
set
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
Marshal.WriteInt32(VariableInCPtr, value);
}
}
public LibTutorial() : base(Path.GetDirectoryName(typeof(LibTutorial).Assembly.Location),
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
VariableInCPtr = GetSymbolPointer("VariableInC");
}
static void Main()
{
LibTutorial lib = new LibTutorial();
Console.WriteLine(lib.VariableInC);
}
}
}
using System.IO;
using System.Runtime.InteropServices;
namespace Tutorial1
{
public class LibTutorial : DLSupport
{
IntPtr VariableInCPtr { get; set; }
int VariableInC
{
get
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
return Marshal.ReadInt32(VariableInCPtr);
}
set
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
Marshal.WriteInt32(VariableInCPtr, value);
}
}
public LibTutorial() : base(Path.GetDirectoryName(typeof(LibTutorial).Assembly.Location),
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
VariableInCPtr = GetSymbolPointer("VariableInC");
}
static void Main()
{
LibTutorial lib = new LibTutorial();
Console.WriteLine(lib.VariableInC);
}
}
}
Build and run the project!
If you get an output of 1, it is working! Feel free to change the initial value of that variable in Tutorial.c to vertify. Or better yet, let's get started what make this approach a delegate approach.
You probably have heard about how you can create a variable that contains a function in C#, it's the same principal in P/Invoke with the use of Function Pointers.
So let's define our delegate type for our C Function!
public delegate void IncrementVariableInC_dt();
You'll notice a suffix "_dt" there, that is a suffix for Delegate Type in C# and the reason for that is because you'll also need to initialize a variable using that type and as C# rule goes, you can't have a type and variable sharing the same name.
So let's declare our variable:
IncrementVariableInC_dt IncrementVariableInC;
Now we need to assign that variable in the constructor and this is where GetFunctionDelegate comes in which accepts a generic delegate type and then generate a delegate from Function Pointer based on the symbol name you provided.
IncrementVariableInC = GetFunctionDelegate<IncrementVariableInC_dt>("IncrementVariableInC");
Now let's try using that function in our project!
Insert this line above Console.WriteLine line:
lib.IncrementVariableInC();
Then build and run the project! You'll get a 2 as an output instead of the initial value of 1!
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Tutorial1
{
public class LibTutorial : DLSupport
{
IncrementVariableInC_dt IncrementVariableInC;
IntPtr VariableInCPtr { get; set; }
int VariableInC
{
get
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
return Marshal.ReadInt32(VariableInCPtr);
}
set
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
Marshal.WriteInt32(VariableInCPtr, value);
}
}
public LibTutorial() : base(Path.GetDirectoryName(typeof(LibTutorial).Assembly.Location),
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
VariableInCPtr = GetSymbolPointer("VariableInC");
IncrementVariableInC = GetFunctionDelegate<IncrementVariableInC_dt>("IncrementVariableInC");
}
public delegate void IncrementVariableInC_dt();
static void Main()
{
LibTutorial lib = new LibTutorial();
lib.IncrementVariableInC();
Console.WriteLine(lib.VariableInC);
}
}
}
using System.IO;
using System.Runtime.InteropServices;
namespace Tutorial1
{
public class LibTutorial : DLSupport
{
IncrementVariableInC_dt IncrementVariableInC;
IntPtr VariableInCPtr { get; set; }
int VariableInC
{
get
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
return Marshal.ReadInt32(VariableInCPtr);
}
set
{
if (VariableInCPtr == IntPtr.Zero)
throw new Exception("Pointer for VariableInC is not assigned!");
Marshal.WriteInt32(VariableInCPtr, value);
}
}
public LibTutorial() : base(Path.GetDirectoryName(typeof(LibTutorial).Assembly.Location),
"libtutorial.so", LoadFlag.RTLD_LAZY)
{
VariableInCPtr = GetSymbolPointer("VariableInC");
IncrementVariableInC = GetFunctionDelegate<IncrementVariableInC_dt>("IncrementVariableInC");
}
public delegate void IncrementVariableInC_dt();
static void Main()
{
LibTutorial lib = new LibTutorial();
lib.IncrementVariableInC();
Console.WriteLine(lib.VariableInC);
}
}
}
This is what a Delegate Approach is and it allows you to interacts with variables in C with the use of DL library which will be explained next chapter as well as the differences between Delegate Approach and DllImport (at least for older version of Dotnet Core.)
If you wish to check out this project, you can checkout from this Repos.






No comments:
Post a Comment