{"id":5790,"date":"2020-08-12T07:00:28","date_gmt":"2020-08-12T14:00:28","guid":{"rendered":"http:\/\/writeasync.net\/?p=5790"},"modified":"2020-08-12T07:54:16","modified_gmt":"2020-08-12T14:54:16","slug":"cross-platform-without-complexity-net-interop","status":"publish","type":"post","link":"http:\/\/writeasync.net\/?p=5790","title":{"rendered":"Cross-platform without complexity: .NET interop"},"content":{"rendered":"<p>The <a href=\"http:\/\/writeasync.net\/?p=5787\">cross-platform project has a shared library<\/a> now. Wouldn&#8217;t it be fun to try to build an <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/native-interop\/pinvoke\">interop project using P\/Invoke<\/a> with this library? To account for the addition of .NET projects, we&#8217;ll need to revisit the project structure:<\/p>\n<ul>\n<li><strong>inc<\/strong> <em>(public header file root)<\/em>\n<ul>\n<li><strong>core<\/strong> <em>(header files for `core` lib)<\/em><\/li>\n<li><strong>lib<\/strong> <em>(header files for shared `lib`)<\/em><\/li>\n<\/ul>\n<\/li>\n<li><strong>src<\/strong> <em>(source code root)<\/em>\n<ul>\n<li><strong>app<\/strong> <em>(executable target)<\/em><\/li>\n<li><strong>core<\/strong> <em>(static lib target)<\/em><\/li>\n<li><ins><strong>interop<\/strong> <em>(.NET interop proj)<\/em><\/ins><\/li>\n<li><strong>lib<\/strong> <em>(shared lib target)<\/em><\/li>\n<\/ul>\n<\/li>\n<li><strong>test<\/strong> <em>(test code root)<\/em>\n<ul>\n<li><ins><strong>core<\/strong> <em>(test for `core` lib)<\/ins><\/em>\n<li><ins><strong>interop<\/strong> <em>(test for interop proj)<\/ins><\/em>\n<\/ul>\n<\/li>\n<\/ul>\n<p>While it is possible to <a href=\"https:\/\/stackoverflow.com\/questions\/2074144\/generate-c-sharp-project-using-cmake\">use CMake to build .NET projects<\/a>, this typically relies on Visual Studio-based generators. If we want to preserve our cross-platform experience, why not just directly use <a href=\"https:\/\/dotnet.microsoft.com\/\">the .NET SDK<\/a> which is already cross-platform?<\/p>\n<p>Let&#8217;s start with the project files. We need a few <a href=\"https:\/\/github.com\/Microsoft\/MSBuildSdks\/tree\/master\/src\/Traversal\">traversal projects<\/a> to easily build the whole tree without relying on a .sln file. First, the top-level <code>dirs.proj<\/code>:<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;Project Sdk=&quot;Microsoft.Build.Traversal&quot;&gt;\r\n  &lt;ItemGroup&gt;\r\n    &lt;ProjectReference Include=&quot;src\\dirs.proj&quot; \/&gt;\r\n    &lt;ProjectReference Include=&quot;test\\dirs.proj&quot; \/&gt;\r\n  &lt;\/ItemGroup&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>Next, <code>src\\dirs.proj<\/code>:<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;Project Sdk=&quot;Microsoft.Build.Traversal&quot;&gt;\r\n  &lt;ItemGroup&gt;\r\n    &lt;ProjectReference Include=&quot;interop\\Sample.Interop.csproj&quot; \/&gt;\r\n  &lt;\/ItemGroup&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>Finally, <code>test\\dirs.proj<\/code>:<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;Project Sdk=&quot;Microsoft.Build.Traversal&quot;&gt;\r\n  &lt;ItemGroup&gt;\r\n    &lt;ProjectReference Include=&quot;interop\\Sample.Interop.Test.csproj&quot; \/&gt;\r\n  &lt;\/ItemGroup&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>To make sure the proper project SDKs are loaded, we also want to put <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/core\/tools\/global-json?tabs=netcore3x\"><code>global.json<\/code><\/a> at the root:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\n{\r\n  &quot;sdk&quot;: {\r\n    &quot;version&quot;: &quot;3.1.302&quot;\r\n  },\r\n  &quot;msbuild-sdks&quot;: {\r\n    &quot;Microsoft.Build.Traversal&quot; : &quot;2.0.52&quot;\r\n  }\r\n}\r\n<\/pre>\n<p>The <code>Sample.Interop.csproj<\/code> will be a simple .NET Core 3.1 class library. Owing to the simplifications in the <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/core\/project-sdk\/overview#project-files\">SDK-style project format<\/a>, the file is minimal:<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;\r\n  &lt;PropertyGroup&gt;\r\n    &lt;TargetFramework&gt;netcoreapp3.1&lt;\/TargetFramework&gt;\r\n  &lt;\/PropertyGroup&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>For <code>Sample.Interop.Test.csproj<\/code>, we&#8217;ll use xUnit.net (the <a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/master\/docs\/workflow\/testing\/libraries\/testing.md\">framework of choice for the .NET Runtime project<\/a> itself):<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;\r\n  &lt;PropertyGroup&gt;\r\n    &lt;TargetFramework&gt;netcoreapp3.1&lt;\/TargetFramework&gt;\r\n    &lt;IsPackable&gt;false&lt;\/IsPackable&gt;\r\n  &lt;\/PropertyGroup&gt;\r\n  &lt;ItemGroup&gt;\r\n    &lt;PackageReference Include=&quot;FluentAssertions&quot; Version=&quot;5.10.3&quot; \/&gt;\r\n    &lt;PackageReference Include=&quot;Microsoft.NET.Test.Sdk&quot; Version=&quot;16.7.0&quot; \/&gt;\r\n    &lt;PackageReference Include=&quot;xunit&quot; Version=&quot;2.4.1&quot; \/&gt;\r\n    &lt;PackageReference Include=&quot;xunit.runner.visualstudio&quot; Version=&quot;2.4.3&quot;&gt;\r\n      &lt;PrivateAssets&gt;all&lt;\/PrivateAssets&gt;\r\n      &lt;IncludeAssets&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;\/IncludeAssets&gt;\r\n    &lt;\/PackageReference&gt;\r\n    &lt;PackageReference Include=&quot;coverlet.collector&quot; Version=&quot;1.3.0&quot;&gt;\r\n      &lt;PrivateAssets&gt;all&lt;\/PrivateAssets&gt;\r\n      &lt;IncludeAssets&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;\/IncludeAssets&gt;\r\n    &lt;\/PackageReference&gt;\r\n  &lt;\/ItemGroup&gt;\r\n  &lt;ItemGroup&gt;\r\n    &lt;ProjectReference Include=&quot;..\\..\\src\\interop\\Sample.Interop.csproj&quot; \/&gt;\r\n  &lt;\/ItemGroup&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>Our goal is to create an interop wrapper for the native shared library. We will specify its behavior in <code>test\\interop\\SampleWrapperTest.cs<\/code>:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\nnamespace Sample.Interop.Test\r\n{\r\n    using FluentAssertions;\r\n    using Xunit;\r\n\r\n    public sealed class SampleWrapperTest\r\n    {\r\n        &#x5B;Fact]\r\n        public void GetName()\r\n        {\r\n            using var hello = new SampleWrapper(&quot;world&quot;);\r\n\r\n            hello.Name.Should().Be(&quot;world&quot;);\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>Before we actually integrate with the shared library, we can create a placeholder implementation with no dependencies in <code>src\\interop\\SampleWrapper.cs<\/code>:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\nnamespace Sample.Interop\r\n{\r\n    using System;\r\n\r\n    public sealed class SampleWrapper : IDisposable\r\n    {\r\n        public SampleWrapper(string name)\r\n        {\r\n            this.Name = name;\r\n        }\r\n\r\n        public string Name { get; private set; }\r\n\r\n        public void Dispose()\r\n        {\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>At this point, we can validate that everything works by running <code>dotnet build<\/code>:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nMicrosoft (R) Build Engine version 16.6.0+5ff7b0c9e for .NET Core\r\nCopyright (C) Microsoft Corporation. All rights reserved.\r\n\r\n  Determining projects to restore...\r\n  Restored X:\\root\\CMakeSampleVS\\test\\dirs.proj (in 244 ms).\r\n  Restored X:\\root\\CMakeSampleVS\\dirs.proj (in 244 ms).\r\n  Restored X:\\root\\CMakeSampleVS\\src\\dirs.proj (in 244 ms).\r\n  Restored X:\\root\\CMakeSampleVS\\src\\interop\\Sample.Interop.csproj (in 244 ms).\r\n  Restored X:\\root\\CMakeSampleVS\\test\\interop\\Sample.Interop.Test.csproj (in 770 ms).\r\n  Sample.Interop -&gt; X:\\root\\CMakeSampleVS\\src\\interop\\bin\\x64\\Debug\\netcoreapp3.1\\Sample.Interop.dll\r\n  Sample.Interop.Test -&gt; X:\\root\\CMakeSampleVS\\test\\interop\\bin\\x64\\Debug\\netcoreapp3.1\\Sample.Interop.Test.dll\r\n\r\nBuild succeeded.\r\n    0 Warning(s)\r\n    0 Error(s)\r\n\r\nTime Elapsed 00:00:04.49\r\n<\/pre>\n<p>&#8230;followed by <code>dotnet test<\/code>:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nTest run for X:\\root\\CMakeSampleVS\\test\\interop\\bin\\x64\\Debug\\netcoreapp3.1\\Sample.Interop.Test.dll(.NETCoreApp,Version=v3.1)\r\nMicrosoft (R) Test Execution Command Line Tool Version 16.6.0\r\nCopyright (c) Microsoft Corporation.  All rights reserved.\r\n\r\nStarting test execution, please wait...\r\n\r\nA total of 1 test files matched the specified pattern.\r\n\r\nTest Run Successful.\r\nTotal tests: 1\r\n     Passed: 1\r\n Total time: 2.2073 Seconds\r\n<\/pre>\n<p>As promised, the same steps work perfectly on Linux (assuming <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/core\/install\/linux\">.NET SDK is installed<\/a>):<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nTest run for \/mnt\/x\/root\/CMakeSampleVS\/test\/interop\/bin\/Debug\/netcoreapp3.1\/Sample.Interop.Test.dll(.NETCoreApp,Version=v3.1)\r\nMicrosoft (R) Test Execution Command Line Tool Version 16.6.0\r\nCopyright (c) Microsoft Corporation.  All rights reserved.\r\n\r\nStarting test execution, please wait...\r\n\r\nA total of 1 test files matched the specified pattern.\r\n\r\nTest Run Successful.\r\nTotal tests: 1\r\n     Passed: 1\r\n Total time: 1.9103 Seconds\r\n<\/pre>\n<p>As a bonus, that command was run against the targets built from the Windows toolset. \ud83d\ude4c But of course, we are not done yet. The first problem is that .NET build outputs go into the source folder by default. Look at all these untracked files in <code>git status<\/code>:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nUntracked files:\r\n  (use &quot;git add &lt;file&gt;...&quot; to include in what will be committed)\r\n        obj\/\r\n        src\/interop\/bin\/\r\n        src\/interop\/obj\/\r\n        src\/obj\/\r\n        test\/interop\/bin\/\r\n        test\/interop\/obj\/\r\n        test\/obj\/\r\n<\/pre>\n<p>Oh, dear. To align with the CMake experience, we want a <a href=\"https:\/\/github.com\/aarnott\/ReadOnlySourceTree\">read-only source tree<\/a>. Luckily, there is an easy enough set of workarounds we can do to retarget all projects, as suggested in a <a href=\"https:\/\/github.com\/dotnet\/sdk\/issues\/867\">.NET SDK GitHub issue<\/a>. We need to create a <code>Directory.Build.props<\/code> file at the root as follows:<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;Project&gt;\r\n  &lt;PropertyGroup&gt;\r\n    &lt;_ProjectDir&gt;$(MSBuildProjectDirectory)\\&lt;\/_ProjectDir&gt;\r\n    &lt;_RelativeDir&gt;$(_ProjectDir.Substring($(MSBuildThisFileDirectory.Length)))\\&lt;\/_RelativeDir&gt;\r\n    &lt;_RootDir&gt;$(MSBuildThisFileDirectory)out\\&lt;\/_RootDir&gt;\r\n    &lt;BaseIntermediateOutputPath&gt;$(_RootDir)obj\\$(_RelativeDir)_$(MSBuildProjectName)\\&lt;\/BaseIntermediateOutputPath&gt;\r\n    &lt;IntermediateOutputPath&gt;$(BaseIntermediateOutputPath)$(Configuration)\\&lt;\/IntermediateOutputPath&gt;\r\n    &lt;BaseOutputPath&gt;$(_RootDir)build\\&lt;\/BaseOutputPath&gt;\r\n    &lt;_OutPrefix&gt;Linux-Clang&lt;\/_OutPrefix&gt;\r\n    &lt;_OutPrefix Condition=&quot;'$(OS)' == 'Windows_NT'&quot;&gt;x64&lt;\/_OutPrefix&gt;\r\n    &lt;_OutSuffix&gt;$(Configuration)&lt;\/_OutSuffix&gt;\r\n    &lt;_OutSuffix Condition=&quot;'$(_OutSuffix)' == ''&quot;&gt;Debug&lt;\/_OutSuffix&gt;\r\n    &lt;OutDir&gt;$(BaseOutputPath)$(_OutPrefix)-$(_OutSuffix)\\&lt;\/OutDir&gt;\r\n  &lt;\/PropertyGroup&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>Note that we&#8217;re reusing the CMake <code>out<\/code> folder as our root path. From there, we specify an <code>obj<\/code> folder as our intermediate output; it is important here that we keep outputs from distinct projects in separate folders as we do not want collisions between generated files such as those created <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/core\/tools\/dotnet-restore\">during restore<\/a>. The final output directory is the same folder as used in CMake for the specific build type and platform target, e.g. <code>Linux-Clang-Debug<\/code> or <code>x64-Release<\/code>. With these changes, we end up with an output folder structure like the following:<\/p>\n<ul>\n<li>out\n<ul>\n<li>build\n<ul>\n<li>x64-Debug<\/li>\n<\/ul>\n<\/li>\n<li>obj\n<ul>\n<li>src\n<ul>\n<li>interop\n<ul>\n<li>_Sample.Interop<\/li>\n<\/ul>\n<\/li>\n<li>_dirs<\/li>\n<\/ul>\n<\/li>\n<li>test\n<ul>\n<li>interop\n<ul>\n<li>_Sample.Interop.Test<\/li>\n<\/ul>\n<\/li>\n<li>_dirs<\/li>\n<\/ul>\n<\/li>\n<li>_dirs<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>In fact, our <code>.gitignore<\/code> file can be simplified down to just these two entries:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n# Build outputs\r\nout\/\r\n\r\n# Visual Studio\r\n.vs\/\r\n<\/pre>\n<p>Since the .NET DLLs and the shared library are in the same folder now, the native dependencies should be implicitly available. (Do keep in mind that we would have to remember to run the <code>build<\/code> script before attempting to load the native library, however.) But there is still one small issue here; shared libraries in Linux typically have the <code>lib<\/code> prefix, while they are otherwise unadorned on Windows. This whole time we were generating <code>libsample-lib.so<\/code> and <code>sample-lib.dll<\/code> on each respective platform without even noticing! Of course, <a href=\"https:\/\/cmake.org\/cmake\/help\/latest\/prop_tgt\/PREFIX.html\">CMake has an option for that<\/a>:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nset_property(TARGET sample-lib\r\n    PROPERTY PREFIX &quot;&quot;\r\n)\r\n<\/pre>\n<p>Now the binary name is the same on both platforms, which means we can finally write the real interop code in <code>src\\interop\\SampleWrapper.cs<\/code>:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\nnamespace Sample.Interop\r\n{\r\n    using System;\r\n    using System.Runtime.InteropServices;\r\n    using Microsoft.Win32.SafeHandles;\r\n\r\n    public sealed class SampleWrapper : IDisposable\r\n    {\r\n        private readonly NativeMethods.SampleHandle handle;\r\n        private readonly Lazy&lt;string&gt; name;\r\n\r\n        public SampleWrapper(string name)\r\n        {\r\n            this.handle = NativeMethods.SampleInit(name);\r\n            this.name = new Lazy&lt;string&gt;(this.GetName);\r\n        }\r\n\r\n        public string Name =&gt; this.name.Value;\r\n\r\n        public void Dispose()\r\n        {\r\n            if (!this.handle.IsClosed)\r\n            {\r\n                this.handle.Dispose();\r\n            }\r\n        }\r\n\r\n        private string GetName()\r\n        {\r\n            IntPtr str = NativeMethods.SampleGetName(this.handle);\r\n            return Marshal.PtrToStringAnsi(str);\r\n        }\r\n\r\n        private static class NativeMethods\r\n        {\r\n            private const string Lib = &quot;sample-lib&quot;;\r\n\r\n            public sealed class SampleHandle : SafeHandleZeroOrMinusOneIsInvalid\r\n            {\r\n                public SampleHandle()\r\n                    : base(true)\r\n                {\r\n                }\r\n\r\n                protected override bool ReleaseHandle()\r\n                {\r\n                    SampleDestroy(this.handle);\r\n                    return true;\r\n                }\r\n            }\r\n\r\n            &#x5B;DllImport(Lib)]\r\n            public static extern SampleHandle SampleInit(\r\n                &#x5B;MarshalAs(UnmanagedType.LPStr)] string name);\r\n\r\n            &#x5B;DllImport(Lib)]\r\n            public static extern IntPtr SampleGetName(SampleHandle p);\r\n\r\n            &#x5B;DllImport(Lib)]\r\n            public static extern void SampleDestroy(IntPtr p);\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>For memory safety, we are implementing <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/garbage-collection\/implementing-dispose\">the Dispose pattern<\/a> and using a <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.runtime.interopservices.safehandle?view=netcore-3.1#examples\">SafeHandle<\/a> to wrap the native pointer. Since the shared library returns a <a href=\"https:\/\/herbsutter.com\/2013\/06\/05\/gotw-91-solution-smart-pointer-parameters\/\">non-owning pointer<\/a> to a <code>char<\/code> for the name string, we have to do <a href=\"https:\/\/stackoverflow.com\/questions\/370079\/pinvoke-for-c-function-that-returns-char\">a bit of marshaling magic<\/a>. We use <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/framework\/performance\/lazy-initialization\">Lazy<\/a> here because the name cannot change after construction and repeatedly doing this interop\/marshal call would be an unnecessary expense.<\/p>\n<p>GitHub repo with the changes: <a href=\"https:\/\/github.com\/bobbymcr\/CMakeSampleVS\/tree\/16ae0c18f0010961c1b638223d5b8f03f6b713db\">CMakeSampleVS<\/a><\/p>\n<p>After running <code>build.cmd<\/code> \/ <code>build.sh<\/code>, <code>dotnet build<\/code>, and <code>dotnet test<\/code>, we see that everything still works on both platforms. However, we haven&#8217;t accounted for a &#8220;one click&#8221; command line build or the VS IDE experience. We can cover these next time.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The cross-platform project has a shared library now. Wouldn&#8217;t it be fun to try to build an interop project using P\/Invoke with this library? To account for the addition of .NET projects, we&#8217;ll need to revisit the project structure: inc&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[107,41],"tags":[],"class_list":["post-5790","post","type-post","status-publish","format-standard","hentry","category-cross-platform","category-tdd"],"_links":{"self":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5790","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5790"}],"version-history":[{"count":7,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5790\/revisions"}],"predecessor-version":[{"id":5800,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5790\/revisions\/5800"}],"wp:attachment":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5790"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5790"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5790"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}