Using the Span<T> in Xamarin Cross-Targeted projects
System.Memory
exists to provide the Span<T>
, Memory<T>
and similar types, for which C# 7.x provided support, and it can enable significant performance improvements where appropriate. While .NET Standard 2.0 does not have support for it in its available API surface, there the System.Memory
NuGet package that can be added to enable it.
As part of the code sharing effort, Mono and Xamarin added support for System.Memory
in the BCL as part of the releases bundled with VS 2019.
While this is a very important step to make Mono and .NET Core very similar in behavior, this brings issues because the Xamarin.iOS
, Xamarin.Mac
and MonoAndroid
target frameworks are not versioned like .NET Core or .NET Standard. The Visual Studio version used to build a project has an impact on the code’s behavior or compatibility that cannot be adjusted as easily as changing a Target Framework version.
VS2019 issues
In VS2019, the following code :
var s = new Span<int>();
with a project file like this:
<Project Sdk="MSBuild.Sdk.Extras/1.6.46">
<PropertyGroup>
<TargetFrameworks>xamarinios10;monoandroid90</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.2"/>
</ItemGroup>
</Project>
does not build in Xamarin.iOS10
, giving this error:
`
error CS0433: The type ‘Span
One easy fix would be to remove the System.Memory
package because mscorlib
already provides it, but it would prevent this library from building with VS15.9 and below.
We could also only add System.Memory
as a normal assembly reference, which would work on both VS versions, but it would break the NuGet package creation and make the consumption of the project’s resulting NuGet package difficult.
Adjusting references during the build
In recent MSBuild, there is a new MSBuildVersion
property which tells the currently running version. While this is far from perfect, we can assume that when a given MSBuild version is installed at the same time as Xamarin for an installation of Visual Studio, the MSBuild version is implicitly tied to the installed Xamarin version.
To keep the PackageReference
visible for the NuGet packing, but remove the assembly reference added by the package, only in VS16.x, we can inject an MSBuild target:
<Project Sdk="MSBuild.Sdk.Extras/1.6.46">
<PropertyGroup>
<TargetFrameworks>xamarinios10</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.2"/>
</ItemGroup>
<Target
Name="VS16_RemoveSystemMemory"
BeforeTargets="FindReferenceAssembliesForReferences"
Condition="'$(MSBuildVersion)' >= '16.0'">
<ItemGroup>
<_ReferencePathToRemove
Include="@(ReferencePath)"
Condition="'%(ReferencePath.NuGetPackageId)'=='System.Memory'" />
<ReferencePath Remove="@(_ReferencePathToRemove)" />
</ItemGroup>
<Message Text="Removing System.Memory for VS 2019 compatibility" Importance="high"/>
</Target>
</Project>
The '$(MSBuildVersion)' >= '16.0'
condition ensure that the target only runs when VS 16.0 is used to build the project. Then we remove the ReferencePath
item that is generated by the loading of the <PackageReference Include="System.Memory" />
.
The target is injected automatically before the FindReferenceAssembliesForReferences
that can be found by looking at the generated binary log, and searching where the ReferencePath
values are manipulated to be provided to the CSC task.
Interestingly enough, this manipulation does not need to be made in Xamarin.iOS head projects, where the reference seems to be automatically adjusted to be removed by the existing targets.