Не так давно у меня был интересный проект на Xamarin Forms для нескольких платформ:
Нам было необходимо создать библиотеку, которая смогла бы подключаться к нескольким нашим проектам: Xamarin.Forms, Android на Java, Cordova, а также позволять сторонним разработчикам использовать наше SDK в своих проектах с минимальными усилиями для интеграции.
Командой было решено написать библиотеку на C и подключать ее к нашим проектам по мере необходимости. Такое решение позволило нам иметь одну кодовую базу для SDK проекта и нам не пришлось дублировать библиотеку отдельно под разные платформы с возможными проблемами при переносе кода и дублировании тестов для покрытия и проверки кода.
Правда в итоге оказалось достаточно тяжело «подружить» библиотеку на C с разными платформами на Xamarin платформе. В данной небольшой статье будет расписано как нам удалось это сделать, и возможно, кому-то это пригодится и позволит сэкономить время на проекте.
Для нашего Xamarin проекта мы сделали еще nuget пакет, который является оберткой над нашей C библиотекой и позволяет в одном месте вносить все необходимые изменения для расширения SDK, а также некоторым образом расширять сам SDK.
Наш Xamarin проект включает в себя четыре платформы, каждая платформа имеет свои архитектуры и на каждой платформе нам необходимо собрать C библиотеку в своем собственном нативном формате файла.
Android
UWP
iOS
MacOS
Нам необходимо собрать нативные файлы для нужных нам платформ и архитектур в режиме релиза.
Собираем проект на С для двух архитектур x86 и x64. После этого у нас будет два *.dll файла, которые нам и нужны.
Чтобы создать нативные файлы для Android проекта. Нам нужно создать Xamarin С++ проект. Добавить наши С файлы и файлы с хедерами в Shared проект. После этого надо собрать проект со всеми необходимыми архитектурами (arm, arm64, x86, x64). Это даст нам *.so файлы для Android проекта.
Чтобы создать нативные файлы для iOS проекта мы могли бы использовать тот же Xamarin С++ проект, что мы использовали для Android, но тут есть нюанс. Нам необходимо соединиться с MacOS, чтобы собрать С++ проект. Но для этого нам необходимо установить vcremote на MacOS. Правда после последних обновлений это сделать сейчас просто невозможно. Может позже Microsoft обратит на это внимание и исправит его установку, но сейчас это к сожалению не так.
Из-за этого нам придется пойти другим путем. В XCode нам надо создать Cocos Touch Static Library проект для iOS. Как это сделать, мы можем прочитать здесь. В этот проект мы добавляем наши файлы из C SDK и собираем проект два раза, чтобы получить нужный нам набор архитектур:
Затем мы можем проверить какие архитектуры включены в наши сборки статической библиотеки, используя команду терминала на MacOS — «lipo». К примеру можем сделать такой вызов:
Результат должен быть таким:
После того как мы приготовили файлы статической библиотеки, мы можем их соединить в один fat файл, со списком всех архитектур в одном файле, снова используя терминальную команду:
На MacOS все будет крайне просто. Нам надо переконвертировать файл статической библиотеки в динамическую, снова используя терминальную команду:
И все. Мы получим нужный нам *.dylib файл.
Так как мы делали nuget пакет и в нем добавляли специфическую логику для Xamarin проекта, то нам необходимо было сделать враппер для C SDK. На C# для подключения C методов нам необходимо использовать атрибут DllImport. Но тут снова есть нюанс. Нам необходимо использовать const для пути нативного С файла. При этом у каждого проекта путь к файлу будет своим и даже название самого файла будет другое. Из-за этого нам пришлось немного изощриться и написать для этого свои обертки.
Итак, наш основной класс, который описывает методы С файла.
Затем для каждой платформы нам необходимо имплементировать абстрактный класс.
Теперь нам необходимо сделать enum файл со списком платформ / архитектур:
И фабрику для использования внутри нашего враппера:
Также нам нужен Init метод для настройки всего что мы создали внутри наших проектов на Xamarin.
Мы копируем сгенерированные файлы библиотек в папки:
И устанавливаем при старте приложения в Init методе нашу архитектуру:
Для Android проекта нам необходимо поправить *.csproj файл, сохранить проект и скопировать *.so файлы в папки. В Android проекте, мы указываем название сгенерированного файла, так как пути к файлам мы прописываем в *.csproj файле. Также нам необходимо помнить следующее при копировании файлов в папки:
Изменения для *.csproj файла:
И устанавливаем архитектуру для nuget пакета:
Нужно добавить сгенерированный *.a fat файл в корневую папку проекта и установить дополнительные инструкции при компилировании проекта (iOS properties => iOS build => Additional mtouch arguments). Устанавливаем следующие инструкции:
Также не забываем в свойствах к *.a файлу указать Build Action как None.
И снова устанавливаем архитектуру для nuget пакета:
Добавляем наш *.dylib файл в Native References проекта и прописываем нужную архитектуру:
После данных манипуляций проекты для всех наших платформ подхватили сгенерированные нативные файлы и мы смогли использовать все функции из нашего СДК внутри проекта.
              
            - 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
- x86
 - x64
 
iOS
- armv7
 - armv7s
 - i386
 - x86_64
 - arm64
 
MacOS
- x86_64
 
Нам необходимо собрать нативные файлы для нужных нам платформ и архитектур в режиме релиза.
Сборка и подготовка нативных файлов 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.aMacOS
На 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 файла:
<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>И устанавливаем архитектуру для 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;После данных манипуляций проекты для всех наших платформ подхватили сгенерированные нативные файлы и мы смогли использовать все функции из нашего СДК внутри проекта.
Комментарии (5)

aslepov78
05.01.2019 23:10Затем для каждой платформы нам необходимо имплементировать абстрактный класс
Бред какой то. Тащить код всех платформ в dll? Условная компиляция ни о чем не говорит?
public abstract class BaseLibraryClass
Абстрактный класс у которого только абстрактные методы? Ребята, на этот случай interface есть.
[DllImport (Path, EntryPoint = «Init», CallingConvention = CallingConvention.Cdecl)]
private static extern int InitExtern (IntPtr value);
Копипаст везде. Кодревью нот пасед. Мой вам совет — не нужно пихать такие методы прямиком в оберточки. Делается чисто статик класс где ТОЛЬКО extern методы. Нужно отделять оберточки с вашим ООП от флат апи. Ну и копипаст тоже решается условной компиляцией.

Dj_Art
05.01.2019 23:25+1Что ж это такое то. UWP таки и под ARM, и под ARM64 есть. И даже во всех гайдлайнах по разработке UWP прописано, что всего две галочки — и работает. Зато вот iOS под i386 есть, замечательно:)
          
 
Imbecile
Прикольно. А в continuous integration вы эту кухню встраивали?
seredaSV Автор
Пока еще к сожалению не настроили CI для всего этого.