Building Facebook.Yoga for UWP and WPF

Eric Rozell

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.

Image DllNotFoundException 1024 215 651

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.

Facebook.Yoga C++ 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.

Image DisableZW 1024 215 698

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:

  1. Copy the code into git
  2. Add a git submodule
  3. 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 AccessViolationExceptionon ARM devices.

The specific API reported to produce the AccessViolationException was the PaddingLeft property getter on the YogaNodeclass.

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.

Facebook.Yoga preprocessor definitions for ARM build

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.

0 comments

Discussion is closed.

Feedback usabilla icon