[Из песочницы] Работа Xamarin c SDK, написанном на C
Не так давно у меня был интересный проект на Xamarin Forms для нескольких платформ:
- Android
- iOS
- UWP
- MacOS
Нам было необходимо создать библиотеку, которая смогла бы подключаться к нескольким нашим проектам: Xamarin.Forms, Android на Java, Cordova, а также позволять сторонним разработчикам использовать наше SDK в своих проектах с минимальными усилиями для интеграции.
Командой было решено написать библиотеку на C и подключать ее к нашим проектам по мере необходимости. Такое решение позволило нам иметь одну кодовую базу для SDK проекта и нам не пришлось дублировать библиотеку отдельно под разные платформы с возможными проблемами при переносе кода и дублировании тестов для покрытия и проверки кода.
Правда в итоге оказалось достаточно тяжело «подружить» библиотеку на C с разными платформами на Xamarin платформе. В данной небольшой статье будет расписано как нам удалось это сделать, и возможно, кому-то это пригодится и позволит сэкономить время на проекте.
Для нашего Xamarin проекта мы сделали еще nuget пакет, который является оберткой над нашей C библиотекой и позволяет в одном месте вносить все необходимые изменения для расширения SDK, а также некоторым образом расширять сам SDK.
Наш Xamarin проект включает в себя четыре платформы, каждая платформа имеет свои архитектуры и на каждой платформе нам необходимо собрать C библиотеку в своем собственном нативном формате файла.
Расширения нативных файлов
- Android — *.so файл;
- Universal Windows Platform (UWP) — *.dll файл;
- iOS — *.a файл (файл статической библиотеки, которая по факту является fat файлом, в котором будут хранится все необходимые нам архитектуры);
- MacOS — *.dylib файл (файл динамической библиотеки)
Возможные архитектуры на разных платформах
Android
- arm
- arm64
- x86
- x64
UWP
iOS
- armv7
- armv7s
- i386
- x86_64
- arm64
MacOS
Нам необходимо собрать нативные файлы для нужных нам платформ и архитектур в режиме релиза.
Сборка и подготовка нативных файлов SDK
Universal Windows Platform (UWP)
Собираем проект на С для двух архитектур x86 и x64. После этого у нас будет два *.dll файла, которые нам и нужны.
Android
Чтобы создать нативные файлы для Android проекта. Нам нужно создать Xamarin С++ проект. Добавить наши С файлы и файлы с хедерами в Shared проект. После этого надо собрать проект со всеми необходимыми архитектурами (arm, arm64, x86, x64). Это даст нам *.so файлы для Android проекта.
iOS
Чтобы создать нативные файлы для iOS проекта мы могли бы использовать тот же Xamarin С++ проект, что мы использовали для Android, но тут есть нюанс. Нам необходимо соединиться с MacOS, чтобы собрать С++ проект. Но для этого нам необходимо установить vcremote на MacOS. Правда после последних обновлений это сделать сейчас просто невозможно. Может позже Microsoft обратит на это внимание и исправит его установку, но сейчас это к сожалению не так.
Из-за этого нам придется пойти другим путем. В XCode нам надо создать Cocos Touch Static Library проект для iOS. Как это сделать, мы можем прочитать здесь. В этот проект мы добавляем наши файлы из C SDK и собираем проект два раза, чтобы получить нужный нам набор архитектур:
- for iphone simulator
- for iphone
Затем мы можем проверить какие архитектуры включены в наши сборки статической библиотеки, используя команду терминала на MacOS — «lipo». К примеру можем сделать такой вызов:
lipo -info /path_to_your_a_file/lib.a
Результат должен быть таким:
Architectures in the fat file: /path_to_your_a_file/lib.a are : armv7 armv7s i386 x86_64 arm6
После того как мы приготовили файлы статической библиотеки, мы можем их соединить в один fat файл, со списком всех архитектур в одном файле, снова используя терминальную команду:
lipo -create lib_iphone.a lib_iphone_simulator.a -output lib.a
MacOS
На MacOS все будет крайне просто. Нам надо переконвертировать файл статической библиотеки в динамическую, снова используя терминальную команду:
clang -fpic -shared -Wl, -all_load lib.a -o lib.dylib
И все. Мы получим нужный нам *.dylib файл.
Nuget пакет
Так как мы делали nuget пакет и в нем добавляли специфическую логику для Xamarin проекта, то нам необходимо было сделать враппер для C SDK. На C# для подключения C методов нам необходимо использовать атрибут DllImport. Но тут снова есть нюанс. Нам необходимо использовать const для пути нативного С файла. При этом у каждого проекта путь к файлу будет своим и даже название самого файла будет другое. Из-за этого нам пришлось немного изощриться и написать для этого свои обертки.
Итак, наш основной класс, который описывает методы С файла.
public abstract class BaseLibraryClass {
public abstract int Init (IntPtr value);
}
Затем для каждой платформы нам необходимо имплементировать абстрактный класс.
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);
}
Теперь нам необходимо сделать enum файл со списком платформ / архитектур:
public enum PlatformArchitecture {
Undefined,
X86,
X64,
Droid,
Ios,
Mac
}
И фабрику для использования внутри нашего враппера:
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 ();
}
}
}
Также нам нужен Init метод для настройки всего что мы создали внутри наших проектов на Xamarin.
public static class Init {
public static PlatformArchitecture PlatformArchitecture { get; set; }
}
Подключение сгенерированных библиотек к проектам
Universal Windows Platform (UWP)
Мы копируем сгенерированные файлы библиотек в папки:
- lib/x86/lib.dll
- lib/x64/lib.dll
И устанавливаем при старте приложения в Init методе нашу архитектуру:
Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.X64;
Android
Для Android проекта нам необходимо поправить *.csproj файл, сохранить проект и скопировать *.so файлы в папки. В Android проекте, мы указываем название сгенерированного файла, так как пути к файлам мы прописываем в *.csproj файле. Также нам необходимо помнить следующее при копировании файлов в папки:
- armeabi — arm *.so файл
- armeabi-v7a — arm *.so файл
- arm64-v8a — arm64 *.so файл
- x86 — x86 *.so файл
- x64 — x64 *.so файл
Изменения для *.csproj файла:
armeabi
Always
armeabi-v7a
Always
arm64-v8a
Always
x86
Always
x86_64
Always
И устанавливаем архитектуру для nuget пакета:
Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Droid;
iOS
Нужно добавить сгенерированный *.a fat файл в корневую папку проекта и установить дополнительные инструкции при компилировании проекта (iOS properties => iOS build => Additional mtouch arguments). Устанавливаем следующие инструкции:
-gcc_flags "-L${ProjectDir} -llib -force_load ${ProjectDir}/lib.a"
Также не забываем в свойствах к *.a файлу указать Build Action как None.
И снова устанавливаем архитектуру для nuget пакета:
Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Ios;
MacOS
Добавляем наш *.dylib файл в Native References проекта и прописываем нужную архитектуру:
Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Mac;
После данных манипуляций проекты для всех наших платформ подхватили сгенерированные нативные файлы и мы смогли использовать все функции из нашего СДК внутри проекта.