gRPC is a networking framework brought to us by Google, which allows for the direct calling of methods on a remote server over a network. We were tasked recently with adding gRPC to our Xamarin Native applications.
Luckily for us, there is an officially managed NuGet package for us to use. Following our last post, you just need to install the necessary NuGet packages. Android builds just fine, but when you try to build iOS..
…the compiler crashes spectacularly with about 100 linker errors.
The C# gRPC support for Xamarin is still considered experimental, and it seems that not much effort is thrown towards addressing issues affecting the framework. There is a Github issue that talks about this (https://github.com/grpc/grpc/issues/19172), but it’s over a year old and has received no attention from developers who work on gRPC. Based on this, the latest version of Grpc.Core that works with iOS out of the box is 1.20, and the latest stable version as of the time of this writing is 2.31.
If you look through the comments on the Github issue, you’ll see that someone came up with a solution: add <IsCxx>true</IsCxx> to both of the NativeReference tags in the Grpc.Core.targets file that comes down from NuGet. And it works! Adding those tags allows our project to build successfully
That said, needing to edit build files that come down from NuGet by hand in order to get your project to compile is cumbersome and error-prone, to say the least. You would need to redo the edit every time you did a fresh clone or needed to blow out the packages directory while you were trying to fix other build issues (an all too common occurrence with Visual Studio). This also complicates CI/CD pipelines, as the changes need to be applied there as well, and CI/CD pipelines often build from a “clean slate”, ensuring that this edit will need to be performed every single time
The ideal way to handle this is at build-time, and in such a way that the change is applied regardless of the circumstances of the last build (if there even was one). To accomplish this, I turned to MSBuild itself, and created a custom Target in our iOS.csproj file to apply our changes.
A Target in MSBuild is analogous to a task in Gradle; it’s something that needs to occur as part of the build process. Targets are the fundamental building blocks of MSBuild, and are very powerful. You can read more about Targets here: https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-targets?view=vs-2019
Our top-level Target declaration looks like this:
I gave ours the name GrpcCoreShim, as it is descriptive and unique. You can override existing Targets with custom ones by giving them the same name (i.e. if we wanted to entirely override the default BeforeBuild task, we would simply name our custom task BeforeBuild), which is not what I want to do here. The BeforeTargets attribute is a semicolon separated list of other targets that our Target needs to run before. I’ve specified CoreCompile here, because it is a build step prior to making use of the NativeReference tags that we need to edit, but after the erroneous ones have already been “loaded”.
Figuring out the correct ordering (for a BeforeTarget or AfterTarget) depends on the project, platform being built for, version of MSBuild et al, and a million other things. As such, it is difficult to know what to target. Setting your Build log verbosity to “Diagnostic” in Visual Studio will go a long way in helping you figure out where to plug in at.
Getting the correct path
The Grpc.Core.targets file that we are attempting to correct looks like this…
…and the first section of our custom Target looks like this:
Each of the NativeReference tags in the .targets file includes a particular assembly file, located at a path relative to the location of the Grpc.Core.targets file. $(MSBuildThisFileDirectory) is a variable that expands to the absolute path of the directory that the Grpc.Core.targets file lives in, which is under the packages directory in the main project folder. It then uses some relative path references to find the needed assembly files. In order to correctly override these NativeReference tags, we need our replacement tags to match as closely as possible, which includes matching this mixture of absolute and relative pathing.
Like most Xamarin native projects, our iOS project lives in a sub-directory of the main project folder as well. We define a variable, GrpcCoreTargetsRelativePath, as a relative path that points us to the folder that the Grpc.Core.targets file lives in. In the ConvertToAbsolutePath step, we transform GrpcCoreTargetsRelativePath into an absolute path and store it in a variable called GrpcCoreTargetsAbsPath. GrpcCoreTargetAbsPath now has thesame value that $(MSBuildThisFileDirectory) will evaluate to when loading the Grpc.Core.targets file. We will use this value in the next section.
If you are using a different version of Grpc.Core (2.31 at the time of this writing), GrpcCoreTargetsRelativePath will need to be changed to the correct version by hand
Overwriting the NativeReferences
This is where the magic happens. Multiple ItemGroup tags defined in different files, or different sections of the same project file all combine into one giant tag at the end. For the most part, these combinations are additive; however, you are allowed to use the Remove attribute to undo the work of other, earlier loaded ItemGroup tags; which is exactly what we do in the first section of our ItemGroup. This “unloads” the erroneous tags given by the Grpc.Core.targets file.
The next section “reloads” them again, with the missing IsCxx tag added. If you look back at the original file, you’ll see that the Include attribute looks very similar as well, down to the relative pathing, thanks to the work we did when deriving GrpcCoreTargetsAbsPath. The Kind and ForceLoad tags are copied over verbatim, as we don’t need to make any changes there.
When we put it all together it looks like this:
All you need to do is add this block as the bottom element of the root Project tag of your iOS.csproj file.