Reputation: 85996
I have some simple C-code which uses a single global-variable. Obviously this is not thread-safe, so when I call it from multiple threads in C# using P/invoke, things screw up.
How can I either import this function separately for each thread, or make it thread-safe?
I tried declaring the variable __declspec(thread)
, but that caused the program to crash. I also tried making a C++/CLI class, but it doesn't allow member-functions to be __declspec(naked)
, which I need (I'm using inline-assembly). I'm not very experienced writing multi-threaded C++ code, so there might be something I'm missing.
Here is some example code:
C#
[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int SomeFunction(int parameter1, int parameter2);
C++
extern "C"
{
int someGlobalVariable;
int __declspec(naked) _someFunction(int parameter1, int parameter2)
{
__asm
{
//someGlobalVariable read/written here
}
}
int __declspec(dllexport) SomeFunction(int parameter1, int parameter2)
{
return _someFunction(parameter1, parameter2);
}
}
[Edit]: The result of SomeFunction()
must go in some prescribed order based on someGlobalVariable
(think of eg. a PRNG, with someGlobalVariable
as the internal state). So, using a mutex or other sort of lock is not an option - each thread must have its own copy of someGlobalVariable
.
Upvotes: 11
Views: 7399
Reputation: 7282
Personally if the C code was to be called elsewhere I would use a mutex there. If that doesn't float your boat you can lock in .Net quite easily:
static object SomeFunctionLock = new Object();
public static int SomeFunction(int parameter1, int parameter2){
lock ( SomeFunctionLock ){
return _SomeFunction( parameter1, parameter2 );
}
}
[DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
internal static extern int _SomeFunction(int parameter1, int parameter2);
[Edit..]
As pointed out, this serializes access to the function which you can't do yourself in this case. You have some C/C++ code that (wrongly IMO) uses a global for state during the call to the exposed function.
As you have observed that the __declspec(thread)
trick doesn't work here then I would try to pass your state/context back and forth as an opaque pointer like so:-
extern "C"
{
int _SomeOtherFunction( void* pctx, int p1, int p2 )
{
return stuff;
}
// publically exposed library function
int __declspec(dllexport) SomeFunction(int parameter1, int parameter2)
{
StateContext ctx;
return _SomeOtherFunction( &ctx, parameter1, parameter2 );
}
// another publically exposed library function that takes state
int __declspec(dllexport) SomeFunctionWithState(StateContext * ctx, int parameter1, int parameter2)
{
return _SomeOtherFunction( ctx, parameter1, parameter2 );
}
// if you wanted to create/preserve/use the state directly
StateContext * __declspec(dllexport) GetState(void) {
ctx = (StateContext*) calloc( 1 , sizeof(StateContext) );
return ctx;
}
// tidy up
void __declspec(dllexport) FreeState(StateContext * ctx) {
free (ctx);
}
}
And the corresponding C# wrapper as before:
[DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
internal static extern int SomeFunction(int parameter1, int parameter2);
[DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
internal static extern int SomeFunctionWithState(IntPtr ctx, int parameter1, int parameter2);
[DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr GetState();
[DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
internal static extern void FreeState(IntPtr);
Upvotes: 2
Reputation: 2791
The good news, you can create a __declspec(naked)
function as a member of C++ (non-CLI) class:
class A {
int n;
public:
A() { n = 0; }
void f(int n1, int n2);
};
__declspec(naked) void A::f(int n1, int n2)
{
n++;
}
The bad news, you will need COM to be able to use such class. That's right: asm wrapped in C++, wrapped in COM, wrapped in RCW, wrapped in CLR...
Upvotes: 1
Reputation: 217293
A common pattern is to have
The C# side would look like this:
Usage:
var state = new ThreadLocal<SomeSafeHandle>(NativeMethods.CreateSomeState);
Parallel.For(0, 100, i =>
{
var result = NativeMethods.SomeFunction(state.Value, i, 42);
Console.WriteLine(result);
});
Declarations:
internal static class NativeMethods
{
[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern SomeSafeHandle CreateSomeState();
[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int SomeFunction(SomeSafeHandle handle,
int parameter1,
int parameter2);
[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
internal static extern int FreeSomeState(IntPtr handle);
}
SafeHandle magic:
[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
internal class SomeSafeHandle : SafeHandle
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public SomeSafeHandle()
: base(IntPtr.Zero, true)
{
}
public override bool IsInvalid
{
get { return this.handle == IntPtr.Zero; }
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
return NativeMethods.FreeSomeState(this.handle) == 0;
}
}
Upvotes: 8
Reputation: 35881
You can either make sure what you only call _someFunction once at a time in your C# code or change the C code to wrap the access to the global variable in a synchronization primitive like a critical section.
I would recommend changing the C# code rather than the C code, as the C# code is multi-threaded, not the C code.
Upvotes: 2