Skip to content

Type Name Mapping

Tristan Labelle edited this page Aug 12, 2023 · 2 revisions

This page describes swift-winrt's approach to mapping type names from .NET to Swift, especially as it concerns namespaces.

Solution

  • .NET assemblies are mapped to Swift modules one-to-one or many-to-one (this is configurable). For example, the Windows.Foundation.FoundationContract.winmd assembly could map to a WindowsFoundation Swift module, or the various reference contract winmd files could all map to a single WindowsUniversalApi Swift module.
  • .NET types names are mapped to Swift types with following the NamespaceSubnamespace_TypeName format. For example, Windows.UI.Composition.CompositionTarget becomes WindowsUIComposition_CompositionTarget.
  • .NET type parameter arity suffixes are dropped if there is no clash within the current assembly, and otherwise preserved with an _ separator. For example, Windows.Foundation.IVector`1 becomes WindowsFoundation_IVector, but System.Action`1 would become System_Action_1.
  • Alias Swift modules can be generated to mimic using statements. For example, a WindowsUIComposition module can be generated which would import the WindowsUniversalApi module and export typealiases for every type in the relevant namespace using typealias CompositionTarget = WindowsUIComposition_CompositionTarget. This also enables the WindowsUIComposition.CompositionTarget syntax in consumers, which works with protocols too.

Reasoning

Problem

The .NET and Swift namespacing models are different and mostly incompatible:

  • .NET has explicit namespaces, which are essentially an extension of the type name. Languages are expected to provide syntactic sugar to make them implicit at the point of use (using declarations in C#).
  • .NET languages may also offer disambiguation by assembly name (Assembly:: in C#), but such uses are very uncommon.
  • Swift supports disambiguation by module name (Foundation.UUID).
  • Swift supports types nesting and empty enums may be used as a form of namespacing. However:
    • The enum requires a single central declaration, which can then be contributed to by arbitrarily many extension's
    • Protocols cannot be nested into enums or other types (until SE-0404 gets implemented)
    • The language provides no syntactic sugar to bring all nested types into the global scope at the point of use.
      • typealiases can be used to introduce global aliases to a nested type, but they target a single type and can clash.

Also relevant to this topic:

  • Both .NET assemblies and Swift modules package related code and are referenced in a directed acyclic dependency graph.
    • Technically .NET assemblies can have circular dependencies but this is exceedingly rare. It happens between C:\Windows\System32\WinMetadata winmd files, but not in the original Reference or UnionMetadata winmd's from the Windows SDK.
  • .NET supports overloading on the type parameter arity (System.Action vs System.Action`1 vs System.Action`1). Swift does not.

Solution goals

As pertains to namespacing, a Swift WinRT bindings should provide:

  • A bijective mapping of type names that preserves the ability to reference two types having the same short name in different namespaces of the same assembly (Namespace1.MyType vs Namespace2.MyType).
  • An analogue to the concept of using declarations, since using fully qualified .NET names everywhere would be very verbose.
  • As much as possible, no special magic for Windows SDK winmd files.

Constrained choices

Map .NET assemblies to Swift modules one-to-one or many-to-one

.NET assemblies commonly defines types under multiple namespaces that reference one another in cyclic dependency graphs. Since Swift modules must form an acyclic dependency graph, .NET namespaces cannot be mapped to Swift modules directly. Some specific assemblies might define types in multiple namespaces such that all dependencies flow in a single direction (for example, Windows.UI to Windows.Foundation), but this assumption is not generalizable and may be brittle since there is no such enforcement in .NET. Unfortunately, this means that the huge Windows.Foundation.UniversalApiContract.winmd file probably cannot be split into multiple modules.

There is a natural one-to-one mapping from .NET assemblies to Swift modules because both form acyclic dependency graphs. In some instances, however, it may be desirable to combine multiple .NET assemblies into a single Swift module (conceptually ilmerge the .NET assemblies):

  • With Windows SDK reference contract assemblies, whose high granularity doesn't need to be exposed in the binding.
  • With WinMetadata assemblies, because they form dependency cycles. This is essentially the same as using the pre-merged UnionMetadata winmd files.

Do not use enums for namespacing in Swift

Swift does yet (see SE-0404) support nesting protocols in types, so an enum-based namespacing strategy would work for structs, classes, enums and delegates but require a different scheme for interfaces. For consistency, we should have a single namespacing strategy.

Reflect fully qualified .NET names in Swift

.NET assembly authors tend to avoid using the same type names in different namespaces, but this nevertheless happens since it is not enforced. For example, in Windows.Foundation.UniversalApiContract.winmd, we find:

AnimationDirection: Windows.UI.Composition, Windows.UI.Xaml.Controls.Primitives
BackgroundActivatedEventArgs: Windows.ApplicationModel.Activation, Windows.UI.WebUI
CompositionTarget: Windows.UI.Composition, Windows.UI.Xaml.Media
EnteredBackgroundEventArgs: Windows.ApplicationModel, Windows.UI.WebUI
EnteredBackgroundEventHandler: Windows.UI.WebUI, Windows.UI.Xaml
LeavingBackgroundEventArgs: Windows.ApplicationModel, Windows.UI.WebUI
LeavingBackgroundEventHandler: Windows.UI.WebUI, Windows.UI.Xaml
NavigatedEventHandler: Windows.UI.WebUI, Windows.UI.Xaml.Navigation
PackageStatus: Windows.ApplicationModel, Windows.Management.Deployment
Panel: Windows.Devices.Enumeration, Windows.UI.Xaml.Controls
PlaybackRateChangeRequestedEventArgs: Windows.Media, Windows.Media.PlayTo
SuspendingDeferral: Windows.ApplicationModel, Windows.UI.WebUI
SuspendingEventArgs: Windows.ApplicationModel, Windows.UI.WebUI
SuspendingEventHandler: Windows.UI.WebUI, Windows.UI.Xaml
SuspendingOperation: Windows.ApplicationModel, Windows.UI.WebUI

The only generalized solution to avoid name clashes is to generate qualified names in Swift. This could take a few forms:

class Windows_UI_Composition_CompositionTarget
class WindowsUIComposition_CompositionTarget
// Or, shortening common namespace prefixes of the assembly
class UI_Composition_CompositionTarget
class UIComposition_CompositionTarget