Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DllImport with .a file for iOS in MAUI #12675

Closed
melanchall opened this issue Jan 15, 2023 · 14 comments
Closed

DllImport with .a file for iOS in MAUI #12675

melanchall opened this issue Jan 15, 2023 · 14 comments
Labels
platform/iOS 🍎 s/needs-attention Issue has more information and needs another look t/bug Something isn't working

Comments

@melanchall
Copy link

I'm trying to add iOS support in my library. In the library I use functions from native binaries via DllImport attributes. So for example:

[DllImport("libraryname")]
private static extern int Foo();

It works without any problems on Windows (via libraryname.dll) and macOS (via libraryname.dylib). From what I've learned from the discussion with the library user, we need .a file for iOS. The user has prepared a simple test MAUI solution: Testify.zip. In the MainPage.xaml.cs we have this declaration:

[DllImport("fat.a", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
private static extern int Foo();

fat.a has been built from this simple C code:

int Foo() { return 234; }

Here you can get the file: fat.zip. The build process:

xcrun --sdk iphoneos --verbose clang -c code.c -o code_arm64.o -arch arm64
ar -rv code_arm64.a code_arm64.o

xcrun --sdk iphoneos --verbose clang -c code.c -o code_x86_64.o -arch x86_64
ar -rv code_x86_64.a code_x86_64.o

lipo code_x86_64.a code_arm64.a -output fat.a -create
lipo -info fat.a

Also the file is put to the Resources/Raw folder with Copy to Output Directory set to Copy always. We see the file in proper place in the built package on a target device or iOS simulator. But calling Foo we get the exception in runtime: System.DllNotFoundException: fat.a. Please see this comment for more details.

So the question is how to use DllImport with native binaries for iOS in a MAUI project? Is it possible at all?

@drasticactions
Copy link
Contributor

I believe your issue is more of a question of the MaciOS SDK and not an MAUI UI Framework issue, it may be a better fit over at https://github.com/xamarin/xamarin-macios.

You may want to follow the Reference Native Library section in the docs (https://learn.microsoft.com/en-us/xamarin/ios/platform/native-interop), which explains where to put the library and how to link to it. While it's intended for Xamarin.iOS, it should also apply to dotnet iOS / MAUI apps.

@rolfbjarne @dalexsoto Are there other ways to help this user?

@rolfbjarne
Copy link
Member

So the question is how to use DllImport with native binaries for iOS in a MAUI project? Is it possible at all?

Use __Internal (two underscores) as the library name when binding a static library.

This is documented here: https://www.mono-project.com/docs/advanced/pinvoke/

Also the file is put to the Resources/Raw folder with Copy to Output Directory set to Copy always.

Don't do this.

Use a <NativeReference> item instead:

<ItemGroup>
    <NativeReference Include="path/to/fat.a" Kind="Static" />
</ItemGroup>

We see the file in proper place in the built package on a target device or iOS simulator.

You're not supposed to see any .a files in the final app, the .a file is linked into the main executable.

@jsuarezruiz jsuarezruiz added t/bug Something isn't working platform/iOS 🍎 labels Jan 16, 2023
@melanchall
Copy link
Author

@rolfbjarne Thanks for the info! We'll try and let you know if it works or not.

Meanwhile, I wonder how to combine all these facts into single cross-platform codebase. Well, for desktop developement we use dynamic native libraries via DllImport with the library name. For iOS we need to use __Internal. How we can resolve in runtime what name should be applied to the attribute? Via NativeLibrary class?

Also about NativeReference element. Will it be valid for csproj of different types? I mean csproj in .NET Framework application or .NET 7. I'm developing a library and the main artifact of my development is NuGet package. There in the package I have such simple .targets file:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <None Include="$(MSBuildThisFileDirectory)Melanchall_DryWetMidi_Native32.dll">
	  <Visible>false</Visible>
      <Link>Melanchall_DryWetMidi_Native32.dll</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
	<None Include="$(MSBuildThisFileDirectory)Melanchall_DryWetMidi_Native64.dll">
	  <Visible>false</Visible>
      <Link>Melanchall_DryWetMidi_Native64.dll</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
	<None Include="$(MSBuildThisFileDirectory)Melanchall_DryWetMidi_Native64.dylib">
	  <Visible>false</Visible>
      <Link>Melanchall_DryWetMidi_Native64.dylib</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

Will it be OK if I add <NativeReference Include="path/to/fat.a" Kind="Static" /> here? Will be any problems in some types of csprojs?

@ayaphi
Copy link

ayaphi commented Jan 16, 2023

Hi there,
short enough to read ;-) : rolfbjarne’s advice works - thanks a lot @rolfbjarne !

+++

I am the user who works with melanchall together on this issue.

To clarify for others who stumble upon this kind of issue: the static library fat.a was just copied into the project folder \Resources\Raw (which is normally meant to be used for assets) and VS automatically set the Build Action to MauiAsset, which seems fine at first glance, because it was deployed into the app package at root level.

Despite it’s the wrong approach ;-) as rolfbjarne said – but just to make this comprehensible for other situations.

Its possible to verify this in the local build output folder for the iOS simulator target on the windows machine : \bin\Debug\net7.0-ios\iossimulator-x64\PROJECTNAME.app and not only in the simulator folder on the Mac build & simulator host.

+++

Following rolfbjarne’s instructions, I placed the fat.a in the root directory of the project, modified the csproj manually as described above and adjusted the DllIport statement.

<ItemGroup>
   <NativeReference Include="fat.a" Kind="Static" />
</ItemGroup>
[DllImport("__Internal")]
private static extern int Foo();

Which takes VS out of “visual sync” in the File Properties toolbar window by the way, because it does not recognizes this “file directive” – it shows Build Action = None and Copy To Output Directory = Do not copy. Should be taken care of, so that this does not override the manual edit in the future.

And it works : the code executes with no exception and returns the expected result (which is 234 by the way, as it is executed on a 64-bit Arm iOS simulator).

Another 2 things (beside File Properties tool window “out of visual sync”) that the .NET MAUI team should take a look at :

  1. I get a warning twice during build : Warning MSB3341 with Could not resolve reference "fat.a". If this reference is required by your code, you may get compilation errors.

  2. @rolfbjarne mentioned it should not show up in the output of the app, as its linked in the main module - this seems to be the case, as it works, but fat.a still shows up in the ouptut as well as on the Windows machine at \bin\Debug\net7.0-ios\iossimulator-x64\PROJECTNAME.app as on the Mac build host in the simulator app deployment folder, which is /Users/SOME_USER/Library/Developer/CoreSimulator/Devices/SOME_UUID/data/Containers/Bundle/Application/SOME_UUID/PROJECTNAME.app.

... but it works ! ;-) !

Cheers
Ayaphi

@rolfbjarne
Copy link
Member

2. @rolfbjarne mentioned it should not show up in the output of the app, as its linked in the main module - this seems to be the case, as it works, but fat.a still shows up in the ouptut as well as on the Windows machine at \bin\Debug\net7.0-ios\iossimulator-x64\PROJECTNAME.app as on the Mac build host in the simulator app deployment folder, which is /Users/SOME_USER/Library/Developer/CoreSimulator/Devices/SOME_UUID/data/Containers/Bundle/Application/SOME_UUID/PROJECTNAME.app.

That's weird... the easiest way to debug this is usually to get a binary build log and then search for the library (fat.a) to try to find out which MSBuild target copies it to the app bundle (and that usually makes it obvious what's happening).

I'm developing a library and the main artifact of my development is NuGet package.

For NuGets we'll automatically detect any *.a files from the runtimes/<RuntimeIdentifier>/native directory instead the nupkg and link with them (for example: in the runtimes/iossimulator-x64/native for an x64-simulator build).

If you create a net7.0-ios binding library project (dotnet new iosbinding), and run dotnet pack, we'll put any *.a files from <NativeReference> items in the csproj in the correct location in the resulting nupkg.

@jsuarezruiz jsuarezruiz added the s/needs-info Issue needs more info from the author label Jan 16, 2023
@ghost
Copy link

ghost commented Jan 16, 2023

Hi @melanchall. We have added the "s/needs-info" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

@melanchall
Copy link
Author

@rolfbjarne Thanks! I have a couple of questions.


Here is my csproj file – Melanchall.DryWetMidi.csproj. Can I just put such an element into <ItemGroup Label="Native">:

<None Include="Melanchall_DryWetMidi_Native64.a">
    <PackagePath>runtimes\_rid_\native\</PackagePath>
    <Pack>true</Pack>
</None>

? So I mean can I get rid of special binding library project (dotnet new iosbinding) and just put the file in the package "by hands", just by specifying the path within the package?


And I unfortunately didn't get an answer on the question regarding library name in DllImport attribute. Well, here you can see an example from my library – InputDeviceApi64.cs. You can see a lot of external functions there with

[DllImport(LibraryName, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]

For desktop applications such attribute works:

[DllImport("Melanchall_DryWetMidi_Native64", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]

looking for Melanchall_DryWetMidi_Native64.dll on Windows, Melanchall_DryWetMidi_Native64.dylib on on macOS and Melanchall_DryWetMidi_Native64.so on Linux. But as you said, for iOS we need such declaration of the attribute:

[DllImport("__Internal", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]

So we need to determine in runtime that we are on iOS and switch library name in DllImport somehow to __Internal. What is the recommended way of doing this?


Sorry for probably stupid questions, but it's really hard to find a complete guide on how to implement cross-platform .NET application with native binaries.

Thanks,
Max

@ghost ghost added s/needs-attention Issue has more information and needs another look and removed s/needs-info Issue needs more info from the author labels Jan 16, 2023
@mattleibow
Copy link
Member

mattleibow commented Jan 16, 2023

You may be able to do this:

#if IOS
[DllImport("__Internal", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
#else
[DllImport("Melanchall_DryWetMidi_Native64", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
#endif
private static extern int Foo();

Or following your code:

#if IOS
const string libraryName = "__Internal";
#else
const string libraryName = "Melanchall_DryWetMidi_Native64";
#endif

[DllImport(libraryName, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
private static extern int Foo();

@rolfbjarne may need to confirm the csproj things, but this feels correct:

<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
    <None Include="Melanchall_DryWetMidi_Native64.dll">
        <PackagePath>runtimes\win10-x64\native\</PackagePath>
        <Pack>true</Pack>
    </None>
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
    <NativeReference Include="Melanchall_DryWetMidi_Native64.a" Kind="Static" />
</ItemGroup>

If I understand all the comments, we have separate assemblies for each TFM, so we can write different code and can use the #if IOS conditional to swap out the dll name. And in the csproj, we can also do something with the platform and pack the windows dll and then use the NativeReference component to let ios do its thing.

@rolfbjarne
Copy link
Member

@mattleibow that looks good, but I believe you'll also have to add net7.0-ios to the TargetFrameworks property in order to build the csproj for iOS.

@jurganson
Copy link

So I am also trying something similar and have followed @rolfbjarne's advice in terms of using "__Internal" for the DLL import library name and also using NativeReference import inside the .csproj file for importing my 'library.a' file.

However I am getting compile errors for every method that I have declared in the likes of:
Native linking failed, undefined symbol: _methodNameHere. This symbol was referenced by the managed member MyLibrary.CSharpFile.methodNameHere. Please verify that all the necessary frameworks have been referenced and native libraries linked.

From what I understand I am importing correctly in Visual Studio - however the 'library.a' might not be build correctly (I am building it myself on my remote mac using cmake and XCode). Is there a good way to troubleshoot this kind of error, like exploring the 'library.a' file and seeing if the methods are indeed exposed or somehow verify that the library is indeed valid?

@jurganson
Copy link

So I am also trying something similar and have followed @rolfbjarne's advice in terms of using "__Internal" for the DLL import library name and also using NativeReference import inside the .csproj file for importing my 'library.a' file.

However I am getting compile errors for every method that I have declared in the likes of: Native linking failed, undefined symbol: _methodNameHere. This symbol was referenced by the managed member MyLibrary.CSharpFile.methodNameHere. Please verify that all the necessary frameworks have been referenced and native libraries linked.

From what I understand I am importing correctly in Visual Studio - however the 'library.a' might not be build correctly (I am building it myself on my remote mac using cmake and XCode). Is there a good way to troubleshoot this kind of error, like exploring the 'library.a' file and seeing if the methods are indeed exposed or somehow verify that the library is indeed valid?

Nevermind I just figured it out. Actually found a post on MSDN forums link where I think @rolfbjarne also provided the answer 🥇

In short I ran nm myLibrary.a and figured out that I had mispelled the functions names in my extern method signatures by comparing the .cs file and the output of the nm command.

@melanchall
Copy link
Author

@mattleibow @rolfbjarne Thank you! I have some comments.

Probably this is wrong:

<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
    <None Include="Melanchall_DryWetMidi_Native64.dll">
        <PackagePath>runtimes\win10-x64\native\</PackagePath>
        <Pack>true</Pack>
    </None>
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
    <NativeReference Include="Melanchall_DryWetMidi_Native64.a" Kind="Static" />
</ItemGroup>

I think you've mixed up fragments for csproj of the library and csproj for an end user project. Currently in my csproj (csproj of the library) I have:

<ItemGroup Label="Native">
  <None Include="Melanchall_DryWetMidi_Native32.dll">
    <PackagePath>build\</PackagePath>
    <Pack>true</Pack>
  </None>
  <None Include="Melanchall_DryWetMidi_Native64.dll">
    <PackagePath>build\</PackagePath>
    <Pack>true</Pack>
  </None>
  <None Include="Melanchall_DryWetMidi_Native64.dylib">
    <PackagePath>build\</PackagePath>
    <Pack>true</Pack>
  </None>
  <None Include="Melanchall.DryWetMidi.targets">
    <PackagePath>build\</PackagePath>
    <Pack>true</Pack>
  </None>
</ItemGroup>

This ItemGroup defines how native binaries should go into the package (yes, I use the build subdirectory so the library can be used in old-style csprojs). So as far as I understand I just need to add such an element into this group:

<None Include="Melanchall_DryWetMidi_Native64.a">
    <PackagePath>runtimes\_rid_\native\</PackagePath>
    <Pack>true</Pack>
</None>

(where rid is an appropriate iOS RID). Also from this comment – #12675 (comment) – I've made a conclusion that NativeReference element will be added automatically by NuGet package installation:

For NuGets we'll automatically detect any *.a files from the runtimes//native directory instead the nupkg and link with them (for example: in the runtimes/iossimulator-x64/native for an x64-simulator build).

@rolfbjarne Am I right? Or the comment of @mattleibow is correct and I need to include NativeReference in the csproj of my library?

@ayaphi
Copy link

ayaphi commented Jan 17, 2023

  1. @rolfbjarne mentioned it should not show up in the output of the app, as its linked in the main module - this seems to be the case, as it works, but fat.a still shows up in the ouptut as well as on the Windows machine at \bin\Debug\net7.0-ios\iossimulator-x64\PROJECTNAME.app as on the Mac build host in the simulator app deployment folder, which is /Users/SOME_USER/Library/Developer/CoreSimulator/Devices/SOME_UUID/data/Containers/Bundle/Application/SOME_UUID/PROJECTNAME.app.

That's weird... the easiest way to debug this is usually to get a binary build log and then search for the library (fat.a) to try to find out which MSBuild target copies it to the app bundle (and that usually makes it obvious what's happening).

@rolfbjarne that fat.a is still in the output, is just a build artefact from an older build where I used the wrong approach. A Build -> Clean Solution in VS resolves this weird thing ;-). I checked the local output folder on the Windows machine as well as the deployed app in the folder of the simulator on the Mac build host.

+++

Another question : where is the build output directory for the project on the Mac build host, as its not under the typicall Xcode path (~\Library\Developer\Xcode\DerivedData)?

Are the some docs about the build mechanics for .NET MAUI already existant somewhere, as the project system has changed from its Xamarin iOS roots?

thanks & cheers
Ayaphi

@jfversluis
Copy link
Member

Hey all! I see a lot is going on here which is great! From what I gather the initial issue seems fixed so I will be closing this for now. If I misunderstood, please open a discussion (it looks more like a "how-to" discussion at this point?) or issue on the https://github.com/xamarin/xamarin-macios repo which seems more suitable in this case.

Thanks everyone!

@ghost ghost locked as resolved and limited conversation to collaborators Feb 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
platform/iOS 🍎 s/needs-attention Issue has more information and needs another look t/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

8 participants