[Из песочницы] Работа Xamarin c SDK, написанном на C

habr.png

Не так давно у меня был интересный проект на 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;


После данных манипуляций проекты для всех наших платформ подхватили сгенерированные нативные файлы и мы смогли использовать все функции из нашего СДК внутри проекта.

© Habrahabr.ru