Table of Contents

Developer Guidelines

We're working on it! Stay tuned! Raise PR and that will help us finding good recommendations.

Until then, here follows some unsorted tips. These are not a full reference, but they give some clues on where to look next.

Quick note about compatibility between managed and native parts

There is an interface mechanism between the managed libraries and the firmware. This interface is structured around the methods decorated with the MethodImpl(MethodImplOptions.InternalCall) attribute and around the fields of the classes and structs, because native code accesses these by index. During the build of a managed library the metadata processor generates a native contract hash that characterizes that interface: the native methods (name, parameters and return types) and the field layout of the classes and structs. The hash is stored in the stub files and in the PE file. Changes can happen at both ends without any compatibility issues, as long as the interface does not change. Adding or changing managed methods, or the body of any method, does not change the hash.

There is no native version to bump. When the interface changes the hash changes by itself and the native declaration has to be updated with the new hash, along with the rest of the stubs.

The native code can't break the contract that the managed code was built for: bug fixes and improvements are fine, breaking changes are not. The hash doesn't see what a native method does, so changing the behavior of an existing native method would go unnoticed. If a breaking change is needed, start from the C# API call: add a new method to the C# library (with its own extern declaration if it needs native code), keep the existing one working and bump the version of the library if the managed API breaks. See the versioning policy.

More details about the hash can be found in Native Contract Hash and in Package and assembly versions and the native contract hash.

The versioning of the previous generation of the framework (native methods checksum and native version) is described in the NuGet, assembly and native versions blog post.

How to call native code from managed code

Assuming you want to call from nanoframework's mscorlib (source can be found in CoreLibrary repository) C# code (e.g. System.Number class) some implementation you would like to place in it's nanoCLR (source in nf-interpreter repository) C++ code. Follow steps below:

  1. Build the nf-CoreLibrary solution without making any changes.

  2. Copy these folders somewhere for later use:

    • nanoFramework.CoreLibrary\bin\Debug\Stubs
    • nanoFramework.CoreLibrary.NoReflection\bin\Debug\Stubs
  3. Declare your C++ function in your C# class:

    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern String FormatNative(
       Object value,
       bool isInteger,
       String format,
       String numberDecimalSeparator,
       String negativeSign,
       String numberGroupSeparator,
       int[] numberGroupSizes);
    
  4. Add code which calls the function above as you wish.

  5. There's no native version to change when you add or modify an extern function (or the fields of a class): the build generates the new native contract hash. If the change also breaks the public C# API of the library, that is a managed breaking change and the version of the library has to be bumped, see the versioning policy.

  6. Build the solution.

  7. Compare the nanoFramework.CoreLibrary\bin\Debug\Stubs folder's actual state with the saved one. The files which should have changed:

    • corlib_native.cpp
    • corlib_native.h
    • your class's C++ counterpart, corlib_native_System_Number.cpp in the example

    Do the same with nanoFramework.CoreLibrary.NoReflection\bin\Debug\Stubs too.

  8. Apply the changes you found to the same files under nf-interpreter/src/CLR/CorLib.

    DO NOT overwrite the files there! The files under nf-interpreter may have additional declarations, etc. Open the file and look for #if CONFIG_NF_FEATURE_SUPPORT_REFLECTION lines. There could be more of them. Copy over the diff meaningfully:

    • changes from the nanoFramework.CoreLibrary\bin\Debug\Stubs are going inside the #if CONFIG_NF_FEATURE_SUPPORT_REFLECTION blocks
    • changes from the nanoFramework.CoreLibrary.NoReflection\bin\Debug\Stubs are going inside the #elif !CONFIG_NF_FEATURE_SUPPORT_REFLECTION blocks.

    In corlib_native.cpp also copy the methods lookup table (it has one entry per native method, in the order generated by the build, so replace the whole table instead of adding the new entry by hand) and the native contract hash in the declaration at the end of the file. Each variant has its own hash.

  9. You will find that a stub for the function you declared above will be generated with this signature:

    HRESULT Library_corlib_native_System_Number::
        FormatNative___STATIC__STRING__OBJECT__BOOLEAN__STRING__STRING__STRING__STRING__SZARRAY_I4(CLR_RT_StackFrame &stack)
    
  10. Now you can implement your function.

How to handle the C++ parameter values received from a C# call

Lets see the example method discussed in the (#How-to-call-native-code-from-managed-code) tip. The generated (by lib-CoreLibrary solution build, where you declared your needs as an extern function) C++ stub will have a CLR_RT_StackFrame &stack parameter. The values can be accessed as follows:

object value

// get ref to value container
CLR_RT_HeapBlock *value;
value = &(stack.Arg0());

// perform unboxing operation if necessary
CLR_RT_TypeDescriptor desc;
NANOCLR_CHECK_HRESULT(desc.InitializeFromObject(*value));
NANOCLR_CHECK_HRESULT(value->PerformUnboxing(desc.m_handlerCls));

// get the CLR_DataType of the value in container, like DATATYPE_U1 for a Byte or DATATYPE_R8 for Double
CLR_DataType dataType = value->DataType();

// retrieving the real value depends on dataType above
int32_t int32Value = value->NumericByRef().s4;

bool isInteger

// get value
bool isInteger;
isInteger = (bool)stack.Arg1().NumericByRef().u1;

String format

// get value
char *format;
format = (char *)stack.Arg2().RecoverString();

int[] numberGroupSizes

// get ref to value container
CLR_RT_HeapBlock_Array *numberGroupSizes;
numberGroupSizes = stack.Arg6().DereferenceArray();

// get number of elements
CLR_UINT32 numOfElements = numberGroupSizes->m_numOfElements;

// get the 5th element
// cast necessary, because GetElement declared as CLR_INT8*, 
// but the C# code call placed items of Int32 type into array.
int the5thEelement = *((CLR_INT32 *)numberGroupSizes->GetElement(5));

Returning from C++ function for C# code

Values should be returned via the CLR_RT_StackFrame &stack parameter.

String

char * ret;
// ... assign value to ret ...

// use helper methods to set return value
NANOCLR_SET_AND_LEAVE(stack.SetResult_String(ret));

Returning with an Exception

// see other CLR_E_* defined values
NANOCLR_SET_AND_LEAVE(CLR_E_FAIL);

Not sure on the differences, but there is a NotImplementedStub helper on CLR_RT_StackFrame &stack parameter may be used too:

NANOCLR_SET_AND_LEAVE(stack.NotImplementedStub());

Example managed-native development cycle

The managed-native border crossing development cycle could have significant time penalty. Without any shortcuts the required steps are:

  1. The native code changes should be compiled
  2. The native code changes should be downloaded to the device
  3. The managed code should be compiled
  4. The managed code should be downloaded to the device
  5. The code should be executed.

So two builds, two downloads required and two development environments involved.

Below you can find a suggestion which can be used in cases when the physical device's capabilities (lik GPIO ports, etc.) not affected by the development, only it's execution of the nanoCLR (like the number ToString() implementation) required. It saved me a lot of time.

  1. Write you managed code which requires native code support (see (#How-to-call-native-code-from-managed-code)) but DO NOT declare native part as an extern method. Just declare it as a "normal" private method. Add some simple implementation what supports the actual development state of your managed code, like return a constant what the currently implemented managed feature would expect from native call. Finish your managed coding agains this stub. You can either write tests for your code too because you have an "emulated" native behaviour. No need to leave the managed code development environment meanwhile.
  2. Replace the stub with the correct extern declaration. Rebuild solution to get the appropriate corlib changes as described in (#How-to-call-native-code-from-managed-code).
  3. Switch to native code development environment.
  4. Add minimal implementation to the extern counterpart C++ function: just extract parameters from their CLR form to C++ form (see (#How to handle in C++ parameter values received from C# call)). The goal is: convert the managed-call-specific things into native C++. Forward call with the extracted parameters to a private func with same logical signature. Now you have a "clean" C++ function without any CLR specific parameter handling logic.
  5. Debug once. Check that your "clean" C++ func receives all the parameters appropriatelly from managed call.
  6. Implement the body of "clean" C++ function. At this moment you are not depending on managed call so you can write anywhere and with any development method, using tests to call your function without any need to setup extensive CLR objects just to test the code you are just developing. E.g. you can write and debug your code on https://www.onlinegdb.com/.