Nuget Packages with Native Libraries
This post is about creating a NuGet package which includes native libraries for Android. I am going to assume that you have a few things in place:
- A .NET standard library project with your C# code, which can be compiled as a nuget package.
- A C# Android project which will load and use the library.
At this point, your app should run when you reference the library project directly. If not, take a look at my previous posts. When you are certain your app works as expected, loads the native .so library and uses it, you should remove the direct reference.
Change the library project The library project should already contain a folder sturcture like this
libs\armeabi-v7a\libWhatever.so
Now you should add a build
folder and a .targets
file:
build\Your.Project.ID.targets
The name of this file is important. It should be equal to your project/package ID. If you’re not certain, look in
properties -> Package -> Package ID
While you are at that page, you might want to fill in some other information if you haven’t done that already. A package ID should be unique. For example, when I created a .NET wrapper for the android version of my YSE library, I called it Yse.NET.Android. This means my targets file is called Yse.NET.Android.targets
.
When you try and create a nuget package at this point, the targets file will not be included. We still have to tell Visual Studio what to do with it, which cannot be done from the interface. Open your project’s .csproj
file and add this itemgroup:
<ItemGroup>
<Content Include="build\*.*">
<Pack>true</Pack>
<PackagePath>build;</PackagePath>
</Content>
</ItemGroup>
When you’ve done this and saved the file, be certain to increase the Package Version (you should do this every time you create a new package). Now right click the project and choose pack
. Next, choose publish
.
If this is the first time you publish a package, you want to create a local package repository. This is as easy as creating an empty directory. It does not have to be on the root of your drive, but I recommend an easy to reach place.
When the package is created, it should be visible in your repository. This is a good time to inspect your package. A nuget package is nothing more than a zip file with another extension. There are lots of ways to open up zip files. I prefer using 7-zip. It adds an entry to the file menu, so when I right-click the package, I can select 7-zip -> Open archive
to view it without unpacking.
The important stuff to watch for is:
- The
content
directory: it should contain your native library. - The
contentFiles
directory: it should contain the same library in a subfoldercontentFiles/any/netstandard2.0
- The
build
directory: should contain your targets file.
If the library is not there, be sure that its properties in Visual studio are set to
- Build Action: Content
- Copy to Output Directory: Copy always
If the build folder is not there, open your .csproj
file again and verify that the code above is still there.
The targets file
Now that your package’s files are in place, you should add the contents of the .targets
file. Use this as a starting point:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_DNAGeneticNugetNativePath Condition="'$(_DNAGeneticNugetNativePath)' == ''">$(MSBuildThisFileDirectory)..\</_DNAGeneticNugetNativePath>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == 'MonoAndroid'">
<AndroidNativeLibrary Include="$(_DNAGeneticNugetNativePath)content\libYse.so">
<Link>$(_DNAGeneticNugetNativePath)content\libYse.so</Link>
<Abi>armeabi-v7a</Abi>
</AndroidNativeLibrary>
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == 'MonoAndroid'">
<AndroidNativeLibrary Include="$(_DNAGeneticNugetNativePath)content\libYse.so">
<Link>$(_DNAGeneticNugetNativePath)content\libYse.so</Link>
<Abi>armeabi</Abi>
</AndroidNativeLibrary>
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == 'MonoAndroid'">
<AndroidNativeLibrary Include="$(_DNAGeneticNugetNativePath)content\libYse.so">
<Link>$(_DNAGeneticNugetNativePath)content\libYse.so</Link>
<Abi>arm64-v8a</Abi>
</AndroidNativeLibrary>
</ItemGroup>
</Project>
A few remarks are in place:
-
The first item (PropertyGroup) sets the path to your library files. When your library is added to your android build, this is where Visual Studio looks for your library. It does not look in the nuget package itself, but in the local file repository where the package is installed. Should you notice that visual studio can’t find your library, double check this item.
-
The ItemGroup objects are needed for every platform. You can add several libraries in one itemgroup, but only for the same Abi. Adding more than one Abi object to the same ItemGroup does not seem to work.
-
I’ve noticed that while Android tries different directories to find your library, it does not look in other Abi folders once it has picked one. So for instance if you have a libary which can be found in the armeabi location, Android will not look for the next library in armeabi-v7a any more. And because C# will add some libaries of its own, it is very much possible that your library cannot be found if you only use put it in armeabi.
-
Windows is not case-sensitive, but Android is. If you have a libary called
libName.so
and in the instructions above, you writelibNAME.so
, Visual Studio will find the library and copy it to the output. But the android dll loader is probably still looking forlibName.so
and will not find your library!
Once you’ve saved this file, create a new nuget package.
Load the Nuget Package Now switch to your application project and add your NuGet package. To find it, add the local package repository to NuGet. The package should load without any problems.
Now don’t run your app directly, but deploy it first. (Right-click the project and choose deploy). If your package can’t be found, it is easier to see in the build output when you choose deploy. If so, examine the path that is given and compare it with the information in your .targets file. Create a new package until you get no errors here.
When there are no errors, run the application. You might want to set a breakpoint where your library is called for the first time. When a library cannot be found, you will see lots of debug output showing all the locations where dllopen tries to find your library. This location is different from your .targets file. Don’t worry about all of them being in the arm
folder while we told to use armeabi-v7a
and such. It seems that whichever abi is picked, it will be called arm
.
If your library cannot be found, you might want to try these steps:
-
Go to your
bin/debug
folder and open the apk file manually. Just like a nuget package, this is also a zip file. The library should be insidelib/armeabi
,lib/armeabi-v7a
andlib/arm64-v8a
. If not, double check your targets file and examine the deploy output. -
Examine the debug output very closely. All those lines look the same, but I’ve seen it happen that the library could be found at one point, but it depended on another library (could be as trivial as libc) which could not be loaded. It was not easy to spot that with the overload of debug info at that point.
-
If your library is loaded by another library, it should not be added as an AndroidNativeLibrary, but as an EmbeddedNativeLibrary.
With the steps above, i was able to add my library to a C# android application. At this point I still have trouble using it in Xamarin Forms. I’ll create another post once i figure that out.
Leave a Comment