Czy jest jakiś sposób na stworzenie biblioteki .Net dla Windows i Mac z referencjami zależnymi od platformy?

12

Staramy się opracować wieloplatformową aplikację dla komputerów stacjonarnych (Windows i Mac) za pomocą C #. Aplikacja zawiera ogromną ilość rzeczy zależnych od platformy, a nasz zespół chce, abyśmy napisali cały ten kod w języku C #. Najłatwiejszym sposobem na to byłoby napisanie wrapperów w C ++ i po prostu odwołanie się do bibliotek w kodzie C # i pozwolenie, aby biblioteki C ++ zajmowały się platformą ... niestety, tak nie jest.

Próbuję skonfigurować bibliotekę za pomocą programu Visual Studio dla komputerów Mac. Mam nadzieję, że będę w stanie zapewnić pojedynczy interfejs w jednej bibliotece, aby zapewnić dostęp do natywnej biblioteki tekstu na mowę na bieżącej platformie - najlepiej bez tworzenia dwóch zestawów. Biblioteka zamiany tekstu na mowę w C # dla Windows nie jest dostępna na Macu, więc tak naprawdę nie mamy alternatywnej opcji niż samodzielne udostępnienie pomostu.

Cieszę się, że biblioteka wykonuje wyszukiwanie systemu operacyjnego w celu ustalenia, na którym systemie operacyjnym działa i / lub próbuje załadować kilka bibliotek rodzimych i po prostu użyć biblioteki, która kiedykolwiek się załaduje.

Jeśli trzeba, wystarczy jeden projekt, który pozwoli mi napisać dwie implementacje. Nie znalazłem sposobu na stworzenie projektu biblioteki w Visual Studio dla komputerów Mac, co pozwoli mi jednak to zrobić. Jedyny rodzaj projektu, który pozwoli na wiele implementacji, wydaje się wymagać, aby implementacje były dla systemu iOS lub Android.

Jaśniejsze
źródło
Z jakiego środowiska uruchomieniowego .NET zamierzasz korzystać?
Euforia
2
Xamarin robi coś podobnego. Może z konfiguracjami kompilacji?
Ewan
2
Warto zauważyć, że Visual Studio dla komputerów Mac nie jest tak naprawdę studiem wizualnym, to po prostu nowe studio Xamarin. Może coś jest w dokumentach Xamarin?
richzilla,
3
Najlepszym podejściem będą różne zespoły ... Jeden dla interfejsów i wspólnego kodu, a drugi dla platformy docelowej. Jednak można wymieniać wiele celów i używać instrukcji #if w celu zamiany implementacji. Minusem jest to, że wszystko, co nie jest włączone, nie jest oceniane, więc może łatwo się zepsuć.
Berin Loritsch,
Kiedy mówisz o referencjach zależnych od platformy, czy mówisz o natywnym kodzie czy o całym kodzie C #?
Berin Loritsch,

Odpowiedzi:

4

Masz dobre pytanie Prawdopodobnie występują pewne kompromisy z twoim rozwiązaniem. Ostateczna odpowiedź naprawdę zależy od tego, co rozumiesz przez zależność od platformy. Na przykład, jeśli uruchamiasz proces uruchamiania aplikacji zewnętrznych i po prostu przełączasz się między aplikacjami, prawdopodobnie możesz sobie z tym poradzić bez zbytniej komplikacji. Jeśli rozmawiasz w trybie P / Invoke z bibliotekami natywnymi, musisz zrobić coś więcej. Jednak jeśli łączysz się z bibliotekami, które istnieją tylko na jednej platformie, prawdopodobnie będziesz musiał użyć wielu zestawów.

Aplikacje zewnętrzne

W #iftej sytuacji prawdopodobnie nie będziesz musiał używać instrukcji. Wystarczy skonfigurować kilka interfejsów i mieć jedną implementację na platformę. Użyj fabryki, aby wykryć platformę i zapewnić odpowiednią instancję.

W niektórych przypadkach jest to tylko plik binarny skompilowany dla konkretnej platformy, ale nazwa pliku wykonywalnego i wszystkie parametry są takie same. W takim przypadku chodzi o rozwiązanie właściwego pliku wykonywalnego. W przypadku masowej aplikacji konwertera audio, która może działać w systemie Windows i Linux, miałem statyczny inicjator rozpoznający nazwę binarną.

public class AudioProcessor
{
    private static readonly string AppName = "lame";
    private static readonly string FullAppPath;

    static AudioProcessor()
    {
        var platform = DetectPlatform();
        var architecture = Detect64or32Bits();

        FullAppPath = Path.combine(platform, architecture, AppName);
    }
}

Nic szczególnego tutaj. Po prostu dobre klasyczne klasy.

P / Invoke

P / Invoke jest nieco trudniejsze. Najważniejsze jest to, że musisz upewnić się, że załadowana jest odpowiednia wersja biblioteki natywnej. W systemie Windows P / Invoke SetDllDirectory(). Różne platformy mogą nie wymagać tego kroku. Więc tutaj może być bałagan. Konieczne może być użycie #ifinstrukcji do kontrolowania, które wywołanie służy do kontrolowania rozwiązywania ścieżki biblioteki - szczególnie jeśli dołączasz ją do pakietu dystrybucyjnego.

Łączenie z całkowicie różnymi bibliotekami zależnymi od platformy

Przydaje się tutaj oldschoolowe podejście ukierunkowane na wiele celów. Jednak wiąże się to z dużą brzydotą. W czasach, gdy niektóre projekty próbowały mieć ten sam docelowy DLL DLL Silverlight, WPF i potencjalnie UAP, musiałbyś skompilować aplikację wiele razy z różnymi znacznikami kompilacji. Wyzwanie w przypadku każdej z powyższych platform polega na tym, że chociaż mają one te same koncepcje, platformy są wystarczająco różne, aby obejść te różnice. Właśnie tam wchodzimy do piekła #if.

To podejście wymaga także ręcznej edycji .csprojpliku w celu obsługi referencji zależnych od platformy. Ponieważ .csprojplik jest plikiem MSBuild, można to zrobić w znany i przewidywalny sposób.

# jeśli do diabła

Możesz włączać i wyłączać sekcje kodu za pomocą #ifinstrukcji, dzięki czemu skutecznie radzi sobie z niewielkimi różnicami między aplikacjami. Na powierzchni brzmi jak dobry pomysł. Użyłem go nawet do włączania i wyłączania wizualizacji ramki granicznej w celu debugowania kodu rysunkowego.

Problem z numerem 1 #ifpolega na tym, że żaden z wyłączonych kodów nie jest analizowany przez analizator składni. Być może masz ukryte błędy składniowe lub, co gorsza, błędy logiczne czekające na ponowne skompilowanie biblioteki. Staje się to jeszcze bardziej problematyczne z kodem refaktoryzacji. Coś tak prostego jak zmiana nazwy metody lub zmiana kolejności parametrów normalnie byłaby obsługiwana OK, ale ponieważ parser nigdy nie ocenia niczego wyłączonego przez #ifinstrukcję, nagle złamałeś kod, którego nie zobaczysz, dopóki nie skompilujesz ponownie.

Cały mój kod debugowania, który został napisany w ten sposób, musiał zostać przepisany po serii przeróbek. Podczas przepisywania użyłem globalnej klasy config do włączania i wyłączania tych funkcji. Dzięki temu refaktoryzuje narzędzie, ale takie rozwiązanie nie pomaga, gdy interfejs API jest zupełnie inny.

Moja preferowana metoda

Moją preferowaną metodą, opartą na wielu bolesnych doświadczeniach, a nawet opartą na własnym przykładzie Microsoftu, jest użycie wielu zestawów.

Jeden podstawowy zestaw NetStandard zdefiniowałby wszystkie interfejsy i zawierałby cały wspólny kod. Implementacje zależne od platformy byłyby w osobnym zestawie, który dodawałby funkcje, jeśli byłyby dołączone.

Przykładem takiego podejścia jest nowy interfejs API konfiguracji i obecna architektura tożsamości. Ponieważ potrzebujesz bardziej szczegółowych integracji, po prostu dodajesz te nowe zespoły. Zespoły te zapewniają również funkcje rozszerzeń, aby włączyć się do konfiguracji. Jeśli używasz metody wstrzykiwania zależności, te metody rozszerzeń pozwalają bibliotece zarejestrować swoje usługi.

Jest to jedyny znany mi sposób na uniknięcie #ifpiekła i zaspokojenie zupełnie innego środowiska.

Berin Loritsch
źródło
Ssę w C # (nie używałem C ++ przez około 18 lat i C # przez około pół dnia, to należy się spodziewać). W tym nowym interfejsie konfiguracyjnym ... możesz podać kilka linków do tego. Nie mogę tego znaleźć na własną rękę.
Jaśniejsze
2
docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/… Istnieje wiele pakietów Nuget, aby dodać dodatkowe źródła konfiguracji. Na przykład ze zmiennych środowiskowych, plików JSON itp.
Berin Loritsch,
1
Jest to Microsoft.Extensions.Configurationzestaw interfejsów API.
Berin Loritsch,
1
Oto główny projekt github: github.com/aspnet/Configuration
Berin Loritsch