We’ve been working with BlueJeans to port react-native-windows to WPF to complement the existing support for UWP. Facebook recently open-sourced Yoga, a cross-platform layout library that implements a subset of CSS, namely flexbox. The React Native framework for iOS and Android switched to use Yoga; we saw this as an opportunity to bring greater parity to react-native-windows.
Background
When we first started developing react-native-windows, we considered a number of options to support the same flex layout properties on Windows as were available on iOS and Android. We considered mapping these concepts to XAML panel types (e.g., Grids and StackPanels) but assumed that it would be a very complicated solution. Fortunately, the work done on the flex layout algorithm for iOS, called css-layout, had already been transpiled to Java and C# by the open source community, so we were able to use it.
Later, the flex layout algorithm was reimplemented in C to support a more complete, well-tested, cross-platform implementation of the flexbox specification. The resulting implementation also included a Java API wrapper that was used in React Native for Android. Since the architecture for react-native-windows closely follows that of React Native for Android, we kicked off an effort to port that Java wrapper to C#.
When creating the UWP project, we could have either built the yoga.dll
project as a Windows Runtime Component or DLL. We took the latter approach and built a C# wrapper with P/Invoke for a few reasons. In the transpiled “css-layout” algorithm, we were able to derive subclasses of CssNode
. We wanted to continue to support this pattern, and with C++/CX, you cannot have a managed class inherit from a C++/CX type. Also, while BlueJeans had demonstrated how C++/CX code could be shared with C++/CLI for compatibility with WPF, we wanted to use as simple an approach as possible that mirrored other React Native platforms’ use of Yoga. Using P/Invoke would be the simplest way to target platforms other than WinRT.
The Problem
As mentioned above, the initial .NET wrapper when Yoga was still the “css-layout” project was developed specifically as a UWP library. The C files for Yoga were imported into a Universal Windows DLL project, and a Universal Windows C# class library was used for the P/Invoke declarations and C# API wrapper. Shortly after we started this work, Facebook announced that the project would be rebranded Yoga, and the project has grown well beyond the scope of React Native.
When the project changed to Yoga, we transitioned the C# class library to a .NET Standard library in collaboration with BlueJeans’ engineers. This change was useful because the library could now be used across .NET Framework for WPF and .NET Core for UWP, as well as Xamarin. We also pivoted from the Universal Windows DLL project for C++ to a Win32 project.
Initial testing showed that this approach worked on all target operating systems for react-native-windows (currently, Windows 7, 8.1, and 10). There was also the added benefit that we only had to publish and test the NuGet package for a single .NET flavor for react-native-windows.
The NuGet package we released for Yoga worked great on desktop. However, when we tested the app on the Windows 10 mobile emulator, we got a DllNotFoundException
on the first API call to Yoga.
Looking at the output in bin/x86/Debug
, it was clear that yoga.dll
was, in fact, being copied to the build output directory, so the problem had to be related to the DLL itself. It was strange that the x86 binary worked on desktop but not on the mobile emulator.
Debugging
We first wanted to confirm that this wasn’t a problem with deploying yoga.dll
from an AppX bundle, as the deployment mechanism for mobile devices and emulators was slightly different than it was for desktop. We produced the AppX Bundle from Visual Studio and used the PowerShell script to install the app on desktop to try and reproduce the DllNotFoundException
. Unfortunately, this did not reproduce the issue.
We then found an issue on an open source project related to SQLite that sounded similar to our issue. A user reported that their x86 binary was working fine on desktop, but not on the mobile emulator. The thread concluded that the issue was related to a dynamic reference to the C runtime where a static reference was required. It turned out that it was trivial to change from a dynamic to a statically-linked C runtime by using the /MT
flag. This could be configured from the project build properties in Visual Studio.
However, after we rebuilt yoga.dll
and repackaged it into a NuGet package to ensure it deployed correctly on the mobile emulator, we had the same DllNotFoundException
.
We started taking a closer look at binary itself. We used dumpbin.exe
to confirm that yoga.dll
was in fact compiled for x86. We ran the command below and confirmed this was not the issue.
> dumpbin.exe /headers yoga.dll Microsoft (R) COFF/PE Dumper Version 14.00.24215.1 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file ..\..\runtimes\win-x86\native\yoga.dll PE signature found File Type: DLL FILE HEADER VALUES 14C machine (x86) 6 number of sections 58A24060 time date stamp Mon Feb 13 18:25:20 2017 0 file pointer to symbol table 0 number of symbols E0 size of optional header 2102 characteristics Executable 32 bit word machine DLL
We finally found the issue when we looked at the dependencies of a known compatible binary for UWP C++ DLL. For the working DLL, the dependencies reported by dumpbin.exe
were different than those for our yoga.dll
binary.
The dependents for yoga.dll
were:
> dumpbin /dependents yoga.dll Microsoft (R) COFF/PE Dumper Version 14.00.24215.1 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file yoga.dll File Type: DLL Image has the following dependencies: VCRUNTIME140.dll api-ms-win-crt-heap-l1-1-0.dll api-ms-win-crt-stdio-l1-1-0.dll api-ms-win-crt-math-l1-1-0.dll api-ms-win-crt-runtime-l1-1-0.dll KERNEL32.dll Summary 1000 .data 1000 .gfids 3000 .rdata 1000 .reloc 1000 .rsrc C000 .text
Whereas the dependents for the known compatible binary were:
File Type: DLL Image has the following dependencies: VCRUNTIME140_APP.dll api-ms-win-crt-heap-l1-1-0.dll api-ms-win-crt-stdio-l1-1-0.dll api-ms-win-crt-math-l1-1-0.dll api-ms-win-crt-runtime-l1-1-0.dll api-ms-win-core-profile-l1-1-0.dll api-ms-win-core-processthreads-l1-1-2.dll api-ms-win-core-sysinfo-l1-2-1.dll api-ms-win-core-interlocked-l1-2-0.dll
The clear difference was between VCRUNTIME140.dll
and VCRUNTIME140_APP.dll
. After discovering this issue, we quickly realized we couldn’t use a standard Win32 DLL for UWP in general, even though it was working fine for UWP Desktop. We likely would have found the same issue with builds for ARM devices, Xbox, and HoloLens.
The Solution
Debugging was the hard part. To solve the problem, we had to create a separate C++ project file to produce separate binaries for Win32 and UWP. The two .vcxproj
files were identical except for the globals section.
The globals section of the Win32 DLL looked like:
<PropertyGroup Label="Globals">
<ProjectGuid>{0446C86B-F47B-4C46-B673-C7AE0CFF35D5}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<ProjectName>yoga</ProjectName>
<RootNamespace>Yoga</RootNamespace>
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
<WindowsSDKDesktopARMSupport>true</WindowsSDKDesktopARMSupport>
</PropertyGroup>
The globals section for the UWP DLL instead used:
<PropertyGroup Label="Globals">
...
<ProjectName>yoga</ProjectName>
<RootNamespace>Yoga</RootNamespace>
<AppContainerApplication>true</AppContainerApplication>
<ApplicationType>Windows Store</ApplicationType>
<WindowsTargetPlatformVersion>10.0.10586.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.10240.0</WindowsTargetPlatformMinVersion>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
</PropertyGroup>
We also had to disable the /ZW
flag that would compile the binary to support C++/CX.
NuGet package
We had to decide how we wanted to consume the Yoga dependency in react-native-windows.
Generally, we’ve taken one of three approaches for dependencies in react-native-windows:
- Copy the code into git
- Add a git submodule
- Use NuGet
Copying the code is a perfectly acceptable option. In fact, it’s how the Yoga dependency is managed in the react-native repository. The problem is that we would need a custom solution to copy the code on a semi-regular basis, and we did not want to add more maintenance to the existing release and upgrade process.
Adding a git submodule is another option we considered. We currently use submodules to have a copy of the RNTester folder from react-native locally in react-native-windows. We have also used submodules in the past to take a dependency on a specific version of ChakraCore. However, it still requires an additional build step for react-native-windows, and another repository to clone when checking out the repository.
We preferred the option of consuming the Yoga dependency as a NuGet package because it only adds a negligible amount of maintenance and does not impact the clone and build time of the repository. We also have a number of other NuGet dependencies for react-native-windows already, including System.Reactive and UWP Community Toolkit.
Shipping NuGet packages that support multiple .NET flavors (in this case, .NET Framework and .NET Core) is straightforward. Since there was already a NuGet package for Facebook.Yoga with the .NET Standard framework target, we added an additional target framework to the dependencies section of the .nuspec
:
<package>
<metadata>
...
<dependencies>
<group targetFramework=".NETStandard1.0" />
<group targetFramework="uap10.0" /> <!-- Added this -->
</dependencies>
...
We also needed to ensure the package picked up the additional build outputs built specifically for UWP.
<package>
...
<files>
<file src="Facebook.Yoga.Native.targets" target="build\netstandard1.0"/>
<file src="_._" target="lib\netstandard1.0"/>
<file src="..\Yoga\bin\x86\Release\Yoga.dll" target="runtimes\win-x86\native"/>
<file src="..\Yoga\bin\x86\Release\Yoga.pdb" target="runtimes\win-x86\native"/>
<file src="..\Yoga\bin\x64\Release\Yoga.dll" target="runtimes\win-x64\native"/>
<file src="..\Yoga\bin\x64\Release\Yoga.pdb" target="runtimes\win-x64\native"/>
<file src="..\Yoga\bin\ARM\Release\Yoga.dll" target="runtimes\win8-arm\native"/>
<file src="..\Yoga\bin\ARM\Release\Yoga.pdb" target="runtimes\win8-arm\native"/>
<!-- Begin added for UWP -->
<file src="Facebook.Yoga.Native.targets" target="build\uap10.0"/>
<file src="_._" target="lib\uap10.0"/>
<file src="..\Yoga\bin\Universal\x86\Release\Yoga.dll" target="runtimes\win10-x86\native"/>
<file src="..\Yoga\bin\Universal\x86\Release\Yoga.pdb" target="runtimes\win10-x86\native"/>
<file src="..\Yoga\bin\Universal\x64\Release\Yoga.dll" target="runtimes\win10-x64\native"/>
<file src="..\Yoga\bin\Universal\x64\Release\Yoga.pdb" target="runtimes\win10-x64\native"/>
<file src="..\Yoga\bin\Universal\ARM\Release\Yoga.dll" target="runtimes\win10-arm\native"/>
<file src="..\Yoga\bin\Universal\ARM\Release\Yoga.pdb" target="runtimes\win10-arm\native"/>
<!-- End added for UWP -->
</files>
</package>
Beyond that, we just needed to ensure the build outputs for the UWP and WPF projects were sent to the correct folders and run nuget pack
to produce the package.
It’s unfortunate that we couldn’t use the same set of binaries across WPF and UWP. Having separate binaries means we have a wider set of tests that have to be run to be confident that upgrading the Yoga package does not break behaviors on either WPF or UWP. However, it’s great that NuGet allows us to use a single package.
The solution above can be implemented in a number of ways. We chose to use separate .vcxproj
project files for the UWP build and the WPF build and created separate solution files for UWP and WPF referencing these projects. We could have just as easily used conditional properties in a single .vcxproj
and separate build configurations for UWP and WPF, which would likely simplify the script for producing a NuGet package. For example, we would not have needed to edit the build output for yoga.dll
to include Universal
in the path, as it would have been copied to a directory named for the unique build configuration.
AccessViolationException on ARM Platform
With the updates to the Yoga NuGet package, we were no longer getting a DllNotFoundException
on the Windows 10 Mobile emulators. However, an issue was quickly filed that the Facebook.Yoga
package was producing an AccessViolationException
on ARM devices.
The specific API reported to produce the AccessViolationException
was the PaddingLeft
property getter on the YogaNode
class.
Initial testing showed that many of the other P/Invoke methods were not throwing an AccessViolationException
and that the problem seemed to be related specifically to any native method returning the YogaValue
struct (YGValue
in C).
We tested if this was a general problem for structs on ARM, with the hypothesis being that it could be a data structure alignment issue. However, we found that methods returning simple structs with primitive type members like uint32_t
or float
were not resulting in an AccessViolationException
. The YogaValue
struct has two members, a float
value and an enum representing the value unit type. We were now reasonably certain that the scope of the problem was limited to structs with enum members. A few tests verified that other variations of structs with enum members would produce the same AccessViolationException
.
Our next hypothesis was that the issue was with the number of bytes in the enum itself. We found supporting documentation that enums in ARM optimize the number of bytes used by an enum based on the number of enum members and their underlying value. For example, the YGUnit
enum has only four members, none of which have negative underlying values. According to the documentation we were reading, the enum value would be represented as an unsigned char
. We tried adjusting the enum underlying type of YogaUnit
in C# to byte
, but we were still getting the AccessViolationException
. We also verified that this was not a problem for enums in general, as methods that returned enums or accepted enums as parameters were working fine.
The next thing we tried was to manually marshal the YGValue
to YogaValue
, rather than letting P/Invoke handle this marshaling. We found the System.Runtime.InteropServices.Marshal.PtrToStructure<T>
API, which we hoped would give more information in the resulting exception. With mixed emotions, we found that this API actually worked without throwing any exception. We successfully converted a pointer to a YGValue
to a YogaValue
, which meant that there was nothing wrong with the declaration of the struct in C#. However, this result also meant that there was likely a bug in the P/Invoke mechanism for these kinds of structs on ARM platforms.
Rather than waiting for a fix to .NET, we decided to just patch the problem using the approach of Marshal.PtrToStructure<T>
. We had the luxury of having source code access to the C implementation of Yoga, so we added a few macros that would return a pointer to the struct instead of the struct itself on ARM only.
#ifdef WINARMDLL
#define WIN_STRUCT(type) type*
#define WIN_STRUCT_REF(value) &value
#else
#define WIN_STRUCT(type) type
#define WIN_STRUCT_REF(value) value
#endif
WIN_EXPORT WIN_STRUCT(YGValue) YGNodeStyleGetPadding(const YGNodeRef node, const YGEdge edge);
WIN_STRUCT(YGValue) YGNodeStyleGetPadding(const YGNodeRef node, const YGEdge edge) {
return WIN_STRUCT_REF(node->style.padding[edge]);
}
We defined WINARMDLL in the .vcxproj preprocessor definitions for ARM builds only.
This macro was applied to all functions returning YGValue
. We also made the necessary changes in C#.
using System; namespace Facebook.Yoga { #if WINDOWS_UWP_ARM using YogaValueType = IntPtr; #else using YogaValueType = YogaValue; #endif internal static class Native { ... [DllImport(DllName, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] public static extern YogaValueType YGNodeStyleGetPadding(YGNodeHandle node, YogaEdge edge); ... } }
using System; using System.Runtime.InteropServices; namespace Facebook.Yoga { public partial class YogaNode { ... public YogaValue PaddingLeft { get { return MarshalValue(Native.YGNodeStyleGetPadding(_ygNode, YogaEdge.Left)); } set { ... } } ... #if WINDOWS_UWP_ARM private static YogaValue MarshalValue(IntPtr ptr) { return Marshal.PtrToStructure<YogaValue>(ptr); } #else private static YogaValue MarshalValue(YogaValue value) { return value; } #endif } }
Conclusion
We pushed both the project type fix and the P/Invoke fix to Yoga, and it’s now being used in the latest release of the package on NuGet. The latest package also includes binaries targeted for Xamarin.iOS, Xamarin.Android, and Xamarin.Mac. With the issues of DllNotFoundException
and AccessViolationException
resolved, we were able to upgrade react-native-windows from an old transpiled version of the Facebook.CSSLayout
library to Facebook.Yoga
. The Facebook.Yoga package has been used in all versions of react-native-windows 0.40 and higher.
The pattern we applied to fix the AccessViolationException
on ARM may be applicable to other native libraries for integration with C# via P/Invoke. In our scenario, the YGValue
struct was a data member of the YGNodeRef
, which is allocated on the heap using the YGNodeNew
API. In this case, returning a pointer to the YGValue
would not be problematic; however, in other cases where the method returns a stack-allocated struct, the workaround we use may not be applicable.
To use Yoga for your own .NET projects, you can install the dependency from NuGet. If you’re interested in getting started with react-native-windows, check out our code story on how to build UWP apps with react-native-windows.
The post Building Facebook.Yoga for UWP and WPF appeared first on ISE Developer Blog.