Xamarin work with SDK written in C

Not so long ago I had an interesting project on Xamarin Forms for several platforms:

  • Android
  • iOS
  • Uwp
  • MacOS

We needed to create a library that could connect to several of our projects: Xamarin.Forms, Android in Java, Cordova, and also allow third-party developers to use our SDK in their projects with minimal effort for integration.

The team decided to write a library in C and connect it to our projects as needed. This solution allowed us to have the same code base for the project SDK and we did not have to duplicate the library separately for different platforms with possible problems when porting code and duplicating tests to cover and verify the code.

But in the end, it turned out to be quite difficult to “make friends” with the C library with different platforms on the Xamarin platform. In this small article it will be described how we managed to do this, and it is possible that someone will find this useful and will save time on the project.

For our Xamarin project, we also made a nuget package, which is a wrapper over our C library and allows you to make all the necessary changes for the SDK extension in one place, and also to extend the SDK itself in some way.

Our Xamarin project includes four platforms, each platform has its own architecture and on each platform we need to build the C library in our own native file format.

Native file extensions


  • Android - * .so file;
  • Universal Windows Platform (UWP) - * .dll file;
  • iOS - * .a file (static library file, which in fact is a fat file, which will store all the necessary architecture for us);
  • MacOS - * .dylib file (dynamic library file)

Possible architectures on different platforms


Android

  • arm
  • arm64
  • x86
  • x64

Uwp

  • x86
  • x64

iOS

  • armv7
  • armv7s
  • i386
  • x86_64
  • arm64

MacOS
  • x86_64

We need to collect native files for the platforms and architectures we need in release mode.

Building and Preparing Native SDK Files


Universal Windows Platform (UWP)


We assemble a project in C for two x86 and x64 architectures. After that we will have two * .dll files that we need.

Android


To create native files for the Android project. We need to create a Xamarin C ++ project. Add our C files and header files to the Shared project. After that, you need to build a project with all the necessary architectures (arm, arm64, x86, x64). This will give us * .so files for the Android project.

iOS


To create native files for an iOS project, we could use the same Xamarin C ++ project that we used for Android, but there is a nuance here. We need to connect with MacOS to build a C ++ project. But for this we need to install vcremote on MacOS. True, after the latest updates to do it now is simply impossible. Maybe later Microsoft will pay attention to it and fix its installation, but now it is unfortunately not the case.

Because of this, we have to go the other way. In XCode, we need to create a Cocos Touch Static Library project for iOS. How to do this, we can read here . In this project, we add our files from the C SDK and build the project two times to get the set of architectures we need:

  • for iphone simulator
  • for iphone

Then we can check which architectures are included in our static library builds using the terminal command on MacOS - “lipo”. For example, we can make such a call:

lipo -info /path_to_your_a_file/lib.a

The result should be:

Architectures in the fat file: /path_to_your_a_file/lib.a are : armv7 armv7s i386 x86_64 arm6

After we have prepared the static library files, we can combine them into one fat file, with a list of all the architectures in one file, again using the terminal command:

lipo -create lib_iphone.a lib_iphone_simulator.a -output lib.a

MacOS


On MacOS, everything will be extremely simple. We need to convert the static library file into a dynamic one, using the terminal command again:

clang -fpic -shared -Wl, -all_load lib.a -o lib.dylib

And that's all. We will get the * .dylib file we need.

Nuget package


Since we made a nuget package and added specific logic for the Xamarin project, we needed to do a wrapper for the C SDK. On C # to connect C methods we need to use the DllImport attribute. But here there is a nuance again. We need to use const for the path of the native C file. At the same time, each project will have its own file path and even the name of the file itself will be different. Because of this, we had to refine ourselves a little and write our own wrappers for this.

So, our main class that describes the C file methods.

publicabstractclassBaseLibraryClass{
    publicabstractintInit(IntPtr value);
}

Then for each platform we need to implement an abstract class.

Android


internal classBaseLibraryClassDroid : BaseLibraryClass{
    privateconst string Path = "lib";
    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    privatestatic extern intInitExtern(IntPtr value);
    public override intInit(IntPtr value)=> InitExtern (value);
}

Universal Windows Platform (UWP)


internal classBaseLibraryClassx64 : BaseLibraryClass{
    privateconst string Path = "lib/x64/lib";
    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    privatestatic extern intInitExtern(IntPtr value);
    public override intInit(IntPtr value)=> InitExtern (value);
}

internal classBaseLibraryClassx86 : BaseLibraryClass{
    privateconst string Path = "lib/x86/lib";
    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    privatestatic extern intInitExtern(IntPtr value);
    public override intInit(IntPtr value)=> InitExtern (value);
}

iOS


internal classBaseLibraryClassIOS : BaseLibraryClass{
    privateconst string Path = "__Internal";
    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    privatestatic extern intInitExtern(IntPtr value);
    public override intInit(IntPtr value)=> InitExtern (value);
}

MacOS


publicclassBaseLibraryClassMac : BaseLibraryClass{
    privateconst string Path = "lib";
    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    privatestatic extern intInitExtern(IntPtr value);
    public override intInit(IntPtr value)=> InitExtern (value);
}

Now we need to make an enum file with a list of platforms / architectures:

publicenum PlatformArchitecture {
    Undefined,
    X86,
    X64,
    Droid,
    Ios,
    Mac
}

And a factory for use inside our wrapper:

publicclassSdkCoreFactory{
    publicstatic BaseLibraryClass GetCoreSdkImp(){
        switch (Init.PlatformArchitecture) {
            case PlatformArchitecture.Undefined:
                thrownew BaseLibraryClassInitializationException ();
            case PlatformArchitecture.X86:
                returnnew BaseLibraryClassx86 ();
            case PlatformArchitecture.X64:
                returnnew BaseLibraryClassx64 ();
            case PlatformArchitecture.Droid:
                returnnew BaseLibraryClassDroid ();
            case PlatformArchitecture.Ios:
                returnnew BaseLibraryClassIOS ();
            case PlatformArchitecture.Mac:
                returnnew BaseLibraryClassMac ();
            default:
                thrownew BaseLibraryClassInitializationException ();
        }
    }
}

We also need the Init method to configure everything we have created inside our Xamarin projects.

publicstaticclassInit{
    publicstatic PlatformArchitecture PlatformArchitecture { get; set; }
}

Connection of generated libraries to projects


Universal Windows Platform (UWP)


We copy the generated library files into folders:

  • lib / x86 / lib.dll
  • lib / x64 / lib.dll

And we install our architecture when the application is started in the Init method:

Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.X64;

Android


For the Android project, we need to correct the * .csproj file, save the project and copy the * .so files into folders. In the Android project, we specify the name of the generated file, since we prescribe the paths to the files in the * .csproj file. We also need to remember the following when copying files to folders:

  • armeabi - arm * .so file
  • armeabi-v7a - arm * .so file
  • arm64-v8a - arm64 * .so file
  • x86 - x86 * .so file
  • x64 - x64 * .so file

Changes for * .csproj file:

<ItemGroup>
    <AndroidNativeLibrary Include="lib\armeabi\lib.so">
        <Abi>armeabi</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\armeabi-v7a\lib.so">
        <Abi>armeabi-v7a</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\arm64-v8a\lib.so">
        <Abi>arm64-v8a</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\x86\lib.so">
        <Abi>x86</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\x86_64\lib.so">
        <Abi>x86_64</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
</ItemGroup>

And install the architecture for the nuget package:

Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Droid;

iOS


You need to add the generated * .a fat file to the root folder of the project and set additional instructions when compiling the project (iOS properties => iOS build => Additional mtouch arguments). Install the following instructions:

-gcc_flags "-L${ProjectDir} -llib -force_load ${ProjectDir}/lib.a"

Also, do not forget to specify the Build Action as None in the properties of the * .a file.

And again we set the architecture for the nuget package:

Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Ios;

MacOS


Add our * .dylib file to the Native References project and prescribe the desired architecture:

Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Mac;

After these manipulations, the projects for all our platforms picked up the generated native files and we were able to use all the functions from our KFOR within the project.

Also popular now: