Not so long ago I had an interesting project on Xamarin Forms for several platforms:
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 it, 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 its 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
AndroidUwpiOS- armv7
- armv7s
- i386
- x86_64
- arm64
MacOSWe 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 this 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 twice 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 files of the static library, 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 did the nuget package and added specific logic for the Xamarin project in it, 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. In this case, each project will have its own path to the file 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.
public abstract class BaseLibraryClass { public abstract int Init (IntPtr value); }
Then for each platform we need to implement an abstract class.
Android
internal class BaseLibraryClassDroid : BaseLibraryClass { private const string Path = "lib"; [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)] private static extern int InitExtern (IntPtr value); public override int Init (IntPtr value) => InitExtern (value); }
Universal Windows Platform (UWP)
internal class BaseLibraryClassx64 : BaseLibraryClass { private const string Path = "lib/x64/lib"; [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)] private static extern int InitExtern (IntPtr value); public override int Init (IntPtr value) => InitExtern (value); }
internal class BaseLibraryClassx86 : BaseLibraryClass { private const string Path = "lib/x86/lib"; [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)] private static extern int InitExtern (IntPtr value); public override int Init (IntPtr value) => InitExtern (value); }
iOS
internal class BaseLibraryClassIOS : BaseLibraryClass { private const string Path = "__Internal"; [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)] private static extern int InitExtern (IntPtr value); public override int Init (IntPtr value) => InitExtern (value); }
MacOS
public class BaseLibraryClassMac : BaseLibraryClass { private const string Path = "lib"; [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)] private static extern int InitExtern (IntPtr value); public override int Init (IntPtr value) => InitExtern (value); }
Now we need to make an enum file with a list of platforms / architectures:
public enum PlatformArchitecture { Undefined, X86, X64, Droid, Ios, Mac }
And a factory for use inside our wrapper:
public class SdkCoreFactory { public static BaseLibraryClass GetCoreSdkImp () { switch (Init.PlatformArchitecture) { case PlatformArchitecture.Undefined: throw new BaseLibraryClassInitializationException (); case PlatformArchitecture.X86: return new BaseLibraryClassx86 (); case PlatformArchitecture.X64: return new BaseLibraryClassx64 (); case PlatformArchitecture.Droid: return new BaseLibraryClassDroid (); case PlatformArchitecture.Ios: return new BaseLibraryClassIOS (); case PlatformArchitecture.Mac: return new BaseLibraryClassMac (); default: throw new BaseLibraryClassInitializationException (); } } }
We also need the Init method to configure everything we have created inside our Xamarin projects.
public static class Init { public static 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 fix 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 we set 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 set 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.