-
Notifications
You must be signed in to change notification settings - Fork 167
OR_GREATER preprocessor symbols for TFMs #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
715c86c
OR_GREATER preprocessor symbols for TFMs
terrajobst 0ac9916
Fix order of `#if`
terrajobst 049f7e7
Fix mass edit bug
terrajobst a9d188e
Remove trailing whitespace
terrajobst 9d0363a
Clarify define behavior at the boundary between .NET 5 and .NET Core 3.1
terrajobst 2a19ec7
Clarify changes for the docs
terrajobst 152e49c
Simplify scenario for bait & switch style packages
terrajobst 3c75641
Address code review feedback
terrajobst 45555b1
Clarify behavior for Xamarin Android
terrajobst 48c4a09
Minor fix
terrajobst 365c304
Apply suggestions from code review
5c1cc94
Clarify behavior
terrajobst 37217d5
Specify behavior for DisableImplicitFrameworkDefines
terrajobst File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # OR_GREATER preprocessor symbols for TFMs | ||
|
|
||
| **PM** [Immo Landwerth](https://github.com/terrajobst) | ||
|
|
||
| Today, projects automatically define `#if` symbols based on the friendly TFM | ||
| representation. This includes both versionless- as well as version-specific | ||
| symbols. For .NET 6, we plan to add `XXX_OR_GREATER` variants that are | ||
| accumulative in nature: | ||
|
|
||
| TFM | Existing | Proposed Additions | ||
| --------------------|----------|-------------------------------------------- | ||
| `net5.0` | `NET5_0` | `NET5_0_OR_GREATER` | ||
| `net6.0` | `NET6_0` | `NET6_0_OR_GREATER`, `NET5_0_OR_GREATER` | ||
|
|
||
| The goal is to align the semantics between package folder names and `#if` | ||
| without breaking existing code. | ||
|
|
||
| The new symbols will make it easier to write code that doesn't need to be | ||
| updated each time the project's `TargetFramework`/`TargetFrameworks` property is | ||
| changed. For example, let's say you want a new code path that is triggered for | ||
| .NET 6.0 and greater. This could either be a new API you want to call or relying | ||
| on a new behavior. Today, you need to write code like that but you need to | ||
| consider what version TFMs your project is targeting. Let's say you target .NET | ||
| Framework 4.6.1, .NET Core 3.1, and .NET 5. | ||
|
|
||
| Today, you'd need to write code like this: | ||
|
|
||
| ```C# | ||
| void M() | ||
| { | ||
| #if NET461 | ||
| LegacyNetFxBehavior(); | ||
| #elif NETCOREAPP3_1 | ||
| LegacyNetCoreBehavior(); | ||
| #elif NET | ||
| Net5OrGreaterBehavior(); | ||
| #else | ||
| #error Unhandled TFM | ||
| #endif | ||
| } | ||
| ``` | ||
|
|
||
| Please note that order is important here. You first handle all the legacy TFMs | ||
| and then you handle the new behavior. To avoid breaking that code once you add | ||
| target .NET 6, you use the versionless TFM for .NET 5 and greater platforms | ||
| (`NET`). However, the code still needs to be revised when you add lower versions | ||
| of existing TFMs. | ||
|
|
||
| With this feature, you'll be able to express it like this, which is a lot more | ||
| intuitive: | ||
|
|
||
| ```C# | ||
| void M() | ||
| { | ||
| #if NET5_0_OR_GREATER | ||
| Net5OrGreaterBehavior(); | ||
| #elif NETCOREAPP3_1_OR_GREATER | ||
| LegacyNetCoreBehavior(); | ||
| #elif NET461_OR_GREATER | ||
| LegacyNetFxBehavior(); | ||
| #else | ||
| #error Unhandled TFM | ||
| #endif | ||
| } | ||
| ``` | ||
|
|
||
| The idea is to start from newest to oldest. | ||
|
|
||
| This also makes the code more resilient for cases where you add new frameworks | ||
| with lower versions later. For example, if you start targeting .NET Core 3.0, | ||
| the former code would go down the path for .NET 6 while the latter will fail | ||
| with "Unhandled TFM", which is a lot more logical for the person having to | ||
| maintain the code. | ||
|
|
||
| ## Scenarios and User Experience | ||
|
|
||
| ### Multi-targeting for devices to provide a cross-platform API | ||
terrajobst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Joletta is building a cross-platform library for geolocation. Since there is no | ||
| built-in API for accessing geolocation, she needs to call different APIs for | ||
| different platforms, so she uses `#if` to write different code, depending on the | ||
| OS she is building for, plus a .NET Standard implementation that just throws | ||
| `PlatformNotSupportedException`. | ||
|
|
||
| Since she needs to provide custom implementations per OS, she wants to fail | ||
terrajobst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| whenever a new TFM is added: | ||
|
|
||
| ```C# | ||
| public static class Gps | ||
| { | ||
| public static GpsCoordinate GetCoordinates() | ||
| { | ||
| #if ANDROID | ||
| return CallAndroidApi(); | ||
| #elif IOS | ||
| return CallIOSApi(); | ||
| #elif WINDOWS | ||
| return CallWindowsApi(); | ||
| #elif NETSTANDARD | ||
| throw new PlatformNotSupportedException(); | ||
| #else | ||
| #error Provide implementation | ||
| #endif | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### When running on a later version of .NET, use a newer API | ||
|
|
||
| Zachariah is building a .NET imaging library. In .NET 5, we have added enough | ||
| hardware intrinsics so that he can implement the blur functionality in a much | ||
| faster way. However, he still wants to support .NET Standard 2.0, so he decides | ||
| to use `#if` to multi-target for .NET 5. | ||
|
|
||
| Since he checks for the minimum version of the framework that introduced the | ||
| API, adding a new version wouldn't change this code, so he simply puts the | ||
| fallback logic for the slow version in the `#else` branch, with no call to | ||
| `#error`. | ||
|
|
||
| ```C# | ||
| public class Picture | ||
| { | ||
| public void Blur(float radius) | ||
| { | ||
| #if NET5_0_OR_GREATER | ||
| FastBlurUsingHardwareIntrinsics(); | ||
| #else | ||
| SlowBlurUsingSimpleMath(); | ||
| #endif | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Requirements | ||
terrajobst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### Goals | ||
|
|
||
| * The new symbols don't change semantics of existing code | ||
| * The new symbols allow expressing a `>=` relationship with version numbers | ||
| * The new symbols apply to both frameworks as well as platforms | ||
| * The new symbols are defined for all frameworks that are relevant for package | ||
| authors today, which includes: | ||
| - .NET 5 | ||
| - .NET Core | ||
| - .NET Framework | ||
| - .NET Standard | ||
|
|
||
| ### Non-Goals | ||
|
|
||
| * Making existing code more resilient to TFM changes | ||
|
|
||
| ## Design | ||
|
|
||
| The SDK will define `XXX_OR_GREATER` variants for the following TFMs: | ||
|
|
||
| * **.NET Framework** | ||
| - Applies to `net1.0`-`net4x` and the `NETFRAMEWORK`/`NET` symbols | ||
| - For example, `net4.8` will define `NETFRAMEWORK`, `NET48`, | ||
| `NET48_OR_GREATER`, `NET472_OR_GREATER`..`NET20_OR_GREATER`. | ||
| * .NET Core | ||
| - Applies to `netcoreappX.Y` and the `NETCOREAPP` symbol | ||
| - For example, `netcoreapp3.1` will define `NETCOREAPP`, `NETCOREAPP3_1`, | ||
| `NETCOREAPP3_1_OR_GREATER`..`NETCOREAPP1_0_OR_GREATER`. | ||
| * .NET 5 and later | ||
| - Applies to `netX.Y` and the `NET` symbol | ||
| - For example, `net6.0` will define `NET`, `NET6_0`, `NET6_0_OR_GREATER` and | ||
| `NET5_0_OR_GREATER`. | ||
| - This will also include the corresponding defines a .NET Core 3.1 successor | ||
| would have gotten. For example, `net5.0` will also define `NETCOREAPP`, | ||
| `NETCOREAPP3_1_OR_GREATER`..`NETCOREAPP1_0_OR_GREATER`. | ||
| - This will *neither* define a `NETCOREAPP5_0` nor | ||
| `NETCOREAPP5_0_OR_GREATER`. | ||
| * .NET 5 and later with operating systems | ||
| - The OS flavors will also gain the `XXX_OR_GREATER` variants | ||
| - For example, `net5.0-windows10.0.19041.0` will also define `WINDOWS`, | ||
| `WINDOWS10_0_19041_0`, `WINDOWS10_0_19041_0_OR_GREATER`, etc. | ||
| * Xamarin | ||
| - This covers the existing Xamarin offerings, the new .NET 6-based iOS and | ||
| Android support is handled by the previous sections. | ||
| - The existing Xamarin Apple platforms don't have versioned preprocessor | ||
| symbols, but Android does: `__ANDROID_16__` | ||
| - We don't plan on adding `OR_GREATER` flavors like | ||
| `__ANDROID_16_OR_GREATER__`. Instead, we're only adding the symbols | ||
| described in the previous section for .NET 6. | ||
|
|
||
| Note: The new `XXX_OR_GREATER` variants should not be generated when the existing | ||
| implicit-TFM defines are disabled, i.e. `DisableImplicitFrameworkDefines` is set | ||
| to `true`. | ||
|
|
||
| ### Doc changes | ||
|
|
||
| The [documentation table][docs-table] in the [#if (C# reference)][docs] will | ||
| need to change as follows: | ||
terrajobst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| > | Target Frameworks | Symbols | | ||
| > | ------------------| ------- | | ||
| > | .NET Framework | `NETFRAMEWORK`, `NET48`, `NET472`, `NET471`, `NET47`, `NET462`, `NET461`, `NET46`, `NET452`, `NET451`, `NET45`, `NET40`, `NET35`, `NET20`, `NET48_OR_GREATER`, `NET472_OR_GREATER`, `NET471_OR_GREATER`, `NET47_OR_GREATER`, `NET462_OR_GREATER`, `NET461_OR_GREATER`, `NET46_OR_GREATER`, `NET452_OR_GREATER`, `NET451_OR_GREATER`, `NET45_OR_GREATER`, `NET40_OR_GREATER`, `NET35_OR_GREATER`, `NET20_OR_GREATER` | | ||
| > | .NET Standard | `NETSTANDARD`, `NETSTANDARD2_1`, `NETSTANDARD2_0`, `NETSTANDARD1_6`, `NETSTANDARD1_5`, `NETSTANDARD1_4`, `NETSTANDARD1_3`, `NETSTANDARD1_2`, `NETSTANDARD1_1`, `NETSTANDARD1_0`, `NETSTANDARD2_1_OR_GREATER`, `NETSTANDARD2_0_OR_GREATER`, `NETSTANDARD1_6_OR_GREATER`, `NETSTANDARD1_5_OR_GREATER`, `NETSTANDARD1_4_OR_GREATER`, `NETSTANDARD1_3_OR_GREATER`, `NETSTANDARD1_2_OR_GREATER`, `NETSTANDARD1_1_OR_GREATER`, `NETSTANDARD1_0_OR_GREATER` | | ||
| > | .NET 6 (and .NET Core) | `NET6_0`, `NET5_0`, `NETCOREAPP`, `NETCOREAPP3_1`, `NETCOREAPP3_0`, `NETCOREAPP2_2`, `NETCOREAPP2_1`, `NETCOREAPP2_0`, `NETCOREAPP1_1`, `NETCOREAPP1_0`, `NET6_0_OR_GREATER`, `NET5_0_OR_GREATER`, `NETCOREAPP3_1_OR_GREATER`, `NETCOREAPP3_0_OR_GREATER`, `NETCOREAPP2_2_OR_GREATER`, `NETCOREAPP2_1_OR_GREATER`, `NETCOREAPP2_0_OR_GREATER`, `NETCOREAPP1_1_OR_GREATER`, `NETCOREAPP1_0_OR_GREATER` | | ||
| > | ||
| > **Notes**: | ||
| > | ||
| > * Versionless symbols are defined regardless of the version you're targeting. | ||
| > * Version-specific symbols are only defined for the version you're targeting. | ||
| > * The `XXX_OR_GREATER` symbols are defined for the version you're targeting | ||
| > and all earlier versions. | ||
|
|
||
| [docs-table]: https://github.com/dotnet/docs/blob/master/includes/preprocessor-symbols.md | ||
| [docs]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if#remarks | ||
|
|
||
| ## Q & A | ||
|
|
||
| ### Why don't we just define the version-specific defines for all old versions too? | ||
|
|
||
| We have tried [that design][net5-preprocessor] in the .NET 5 timeframe and | ||
| [ended up backing it out][net5-issue] because it was considered too surprising. | ||
|
|
||
| [net5-preprocessor]: https://github.com/dotnet/designs/blob/main/accepted/2020/net5/net5.md#preprocessor-symbols | ||
| [net5-issue]: https://github.com/dotnet/docs/issues/20692 | ||
|
|
||
| The basic problem was that starting with .NET 5, version-specific defines would | ||
| have behaved differently from the past. That is, when you target `net5.0`, both | ||
| `NETCOREAPP3_1` and `NET5_0` would be defined, and when targeting `net6.0`, | ||
| `NETCOREAPP3_1`, and `NET5_0` would be defined as well. | ||
|
|
||
| Due to a lot of confusion and concern we ended up not shipping this design. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.