You can’t do interops with GHI’s premium products since the source code for their port is unavailable. That’s why I stick to the open-source ports. Dobova posted a great link about interops on the FEZ Hydra. Definitely read through that for the details, however, here’s a high-level idea of what’s going on:
A core part of NETMF is the ability for managed and native code to interact – hence “interops”. People talk about writing “interop code” to do this or that, so we often think of interops as third-party library code. However, it’s important to understand that, actually, interop code is used all over the place inside of NETMF.
Look at the serial port code, for example. When you call the Send() function on a serial port object in NETMF, it’s actually a wrapper for a lower-level Send() function (still managed), which calls an interop function written in C++. This interop function calls the PAL function USART_Write(). This PAL function calls the HAL function CPU_USART_WriteCharToTxBuffer().
It sounds complicated, but the advantage is that, you, as the developer, only need to implement CPU_USART_WriteCharToTxBuffer() for your particular processor, and then call “SerialPort.Send()” in your managed application – all the gross code sitting between those two things have already been implemented generically.
Now, if you go digging through the code, you’ll see that there’s a lot of extra code required to do method checking, function table generation, grab parameters off the stack, and put parameters back on the stack. Luckily, when you enable stub generation in Visual Studio, when you declare a function extern and decorate it with the Interops label appropriately, it will do all the work for you. For example, if you want to implement a singleton class named MyLib with a Transmit function, you’d declare it:
[MethodImpl(MethodImplOptions.InternalCall)]
public extern void Transmit(byte[] data)
if you ticked the “Generate native stubs” checkbox, when you built the library, you’d get a stubs folder in the build output. This stubs folder would contain a function like this:
void MyLib::Transmit( CLR_RT_TypedArray_UINT8 param0, HRESULT &hr )
{
}
That’s easy enough – your byte array turned into a CLR_RT_TypedArray_UINT8 object named param0. You’ll have to look at the specific documentation for it, but this object can be accessed just like any other array, with the advantage of having some extra functionality; i.e., you can do:
void MyLib::Transmit( CLR_RT_TypedArray_UINT8 param0, HRESULT &hr )
{
for(int i=0; i<param0.GetSize(); i++) // GetSize() returns the size of the array
{
MyTransmitFunction(param0[i]); // we can use [] accessor methods, just like C++ arrays
}
}
So, there’s some data types that have some enhanced functionality, but all in all, there’s not a whole lot new to learn here.
One thing you won’t have to mess with, but you should still be aware of: the function above isn’t actually the function that gets called directly. Rather, in your stubs folder, you’ll see some other files that have to do with interop signatures, and a method lookup table, and then a marshall function that looks like this:
HRESULT Library_MyLib::Transmit___STATIC__VOID__SZARRAY_U1( CLR_RT_StackFrame& stack )
{
TINYCLR_HEADER(); hr = S_OK;
{
CLR_RT_TypedArray_UINT8 param0;
TINYCLR_CHECK_HRESULT( Interop_Marshal_UINT8_ARRAY( stack, 0, param0 ) );
MyLib::Transmit( param0, hr );
TINYCLR_CHECK_HRESULT( hr );
}
TINYCLR_NOCLEANUP();
}
That gross piece of code is what TinyCLR actually calls when you call your Transmit function from C#. It grabs the current stack frame, pulls the array off the stack, and calls your function with it. It also passes around the hr parameter which your native code can use to throw exceptions in the managed environment (very useful!).
Basically, with interops, you can do things as “nicely” as the core NETMF functions work – since you’re using the same technology.
Typically, you use native interops to speed up processing of stuff. That sort of stuff – algorithms – is really easy to implement, since it’s just straight-ahead C++. But when you want to start doing hardware stuff, you need to get the PK manual out and figure out the names of functions. For example, if you want to set a GPIO pin high inside of an interop, you have to do:
CPU_GPIO_EnableOutputPin(3, 0); // set A3 as an output pin, and set its initial state to 0.
...
CPU_GPIO_SetPinState(3, 1); // later on, turn the pin on.
Not hard, but definitely underdocumented. For me, the most confusing part was figuring out how to generate an event (like an interupt) in the managed framework from interop code.
You end up writing what’s called an Interrupt Driver which has an Initialize, Enable/Disable, and Cleanup functions. You’d typically have a fourth function that’d actually generate the interrupt. The trick is to call
SaveNativeEventToHALQueue( g_Context, param0, param1 );
where param0 and param1 are ints. Once that is called, you can hook up to that event in the managed environment using NativeEventDispatcher:
NativeEventDispatcher m_evtDataEvent = new NativeEventDispatcher("MyEvent", 0);
m_evtDataEvent.OnInterrupt += m_evtDataEvent_OnInterrupt;
If you need to pass a byte array to the managed environment, the only way to do that is to trigger an interrupt which causes the managed environment to call a “GetData(byte)” sort of function to retrieve the data.