sdk icon indicating copy to clipboard operation
sdk copied to clipboard

Conditional code preprocessing

Open dnfield opened this issue 6 years ago • 21 comments

I'm primarily concerned about this for Flutter, where I can't use dart:mirrors.

As a library author, I would like to be able to publish code that conditionally uses new functionality if it's available in a downstream consumer's SDK version.

I could imagine implementing this by checking:

  1. Whether a compile time variable is set.
  2. Whether a method/property is defined/exists in the Flutter SDK API that my library consumes.

For these examples, assume I want to decode a UTF8 string and use data in that string to draw a circle with a particular type of gradient; in the latest SDK, I can draw exactly that gradient, whereas in older SDKs I want to log a warning and draw something close to the desired gradient (perhaps in a more computationally expensive manner or in a manner that is lower fidelity compared with the expected output now possible in the latest SDK).

For example, using C-style preprocessor notation

#if FLUTTER_SDK >= 0.4.5
// Use new and better methods, use alternatives to deprecated methods, e.g. `utf8.decode`
// or new gradient shading methods that are just getting added.
#else
// Fall back or simply don't provide functionality, or use a method
// that isn't deprecated in older version, e.g. `UTF8.decode`, or using a close match 
// on the gradient or perhaps just skipping the rendering for the unavailable 
// gradient method(s).
#endif

or:

#if IS_DEFINED(sdkClass.newCoolMethod)
// code that uses `sdkClass.newCoolMethod()`
#else
// code that uses some work around, or perhaps does nothing at all
#endif

or, using something like JavaScript:

if (sdkClass.newCoolMethod) { // but no reflection.
  sdkClass.newCoolMethod();
}

Basically, I'd like to avoid needing to maintain multiple versions of my library to support Flutter beta, dev, and/or master channels. I'm OK with users on Beta not getting cutting edge functionality, and I'm even OK with maintaining a build configuration file (or section of pubspec.yaml) to drive this; but I'd like to avoid a very confusing scenario where I have to keep going back and checking if I can migrate functionality to my "beta" tracking package from my "dev" tracking package - just let the compiler include that functionality if it's available in the SDK, and skip it (or use a specified alternative) if it's not.

Although I'm using C-style preprocessing to illustrate here, I'm not looking for a full preprocessor. I'd be very open to other ideas or suggestions that don't involve reflection at runtime.

dnfield avatar May 27 '18 04:05 dnfield

some mechanism like this would also be useful for libraries that come in two flavors: one depending on 'dart:html' and one that does not.

robertmuth avatar May 28 '18 14:05 robertmuth

/cc @leafpetersen @lrhn

a-siva avatar May 30 '18 22:05 a-siva

In the Dart 2 time frame, we will have some (but not all of what you are asking for here). You will be able to conditionally import/export code controlled by a fixed set of platform defined flags. We will not have finer granularity (e.g. platform version) in that time frame (and no current plans for adding it later). Follow https://github.com/dart-lang/sdk/issues/32960 for more details.

leafpetersen avatar May 30 '18 22:05 leafpetersen

So it certainly could be achieved as a preprocessing step before passing to Dart if it's not targetted for the language. My only fear with that is fragmentation (there's the coffeedart preprocessor, and the typedart preprocessor, and the danfielddart preprocessor, etc). That, and the fact that it wouldn't be recognized by the analyzer in any meaningful way.

dnfield avatar May 31 '18 12:05 dnfield

I do want a feature like this. It enables gradual migration of new, breaking, language features.

lrhn avatar Jun 22 '18 15:06 lrhn

BTW: this is the best documentation on conditional imports I could find:

https://medium.com/@dvargahali/dart-2-conditional-imports-update-16147a776aa8

robertmuth avatar Nov 28 '18 03:11 robertmuth

This is coming up as something that would help Flutter framework implement cross-platform features in ways that are especially challenging today - such as supporting both dart:html and dart:io, or supporting Fuchsia specific things that we don't expect will be available for other platforms.

dnfield avatar Mar 01 '19 06:03 dnfield

/cc @amirh

dnfield avatar Mar 01 '19 06:03 dnfield

After having used conditional imports on a few occasions I find them to be rather clumsy. Typically I use them to redirect code to a stub implementation, which usually affects many files.

A much cleaner mechanism in my opinion would allow me to specify this redirection in only place. For example, I would like to say "for this build target, redirect all uses of dart:html to stubbed_out_html.dart"

robertmuth avatar Mar 02 '19 03:03 robertmuth

If you just want to implement an entire API twice, without forwarding from stubs, you can use conditional exports.

library platform_dependent_html;
export "stubbed_out_html.dart"
    if (dart.library.html) "html.dart";

Then you can import this library, and get real HTML if it's there, or stubbed_out_html.dart if not.

We do not provide a way for a build to replace an existing library that someone else has asked for explicitly.

lrhn avatar Mar 04 '19 09:03 lrhn

If you just want to implement an entire API twice, without forwarding from stubs, you can use conditional exports.

I don't think conditional exports are working everywhere currently. #34179

natebosch avatar Mar 04 '19 19:03 natebosch

Much needed feature! And you can find inspiration in Haxe Conditional Compilation it helps us a lot https://haxe.org/manual/lf-condition-compilation.html

VladimirCores avatar Jun 12 '20 18:06 VladimirCores

I'm definetly missing C# pre-processor features, different build configurations defining own symbols and condition compilation with #if statements

maxim-saplin avatar Sep 02 '20 12:09 maxim-saplin

@lrhn has there been any more thought about this recently? I was thinking about this exact proposal in lieu of having a much cleaner way of doing io/html implementations of let's say a service class (with regards to Flutter).

Currently what I have to do to get it working is the following. Not sure if there is a cleaner way for my current approach either. (Bear in mind, this is in a standalone project, specifically not a package)

  1. A stub file with a global method
  • contains a top-level method like MyService getMyServiceInstance() => throw UnimplementedError('this is a stub');
  1. A common interface that defines the service, MyService
  • contains a conditional import that imports the correct implementation (or the stub)
  • contains a factory constructor of MyService that uses the global method as implementation
  1. Implementations for io/web
  • override the global stub method and pass in their own instance

With pre-processor features like in C++/C# I can get rid of the stub file, (the factory constructor) and the separate implementations, therefore keeping everything in one class.

Something like this

#ifdef library.dart.io
import 'dart:io';
#elifdef library.dart.html
import 'dart:html';
#endif

class MyService {
  bool someFunction(){
#ifdef library.dart.io
    return true;
#elifdef library.dart.html
    return false;
#else
    throw UnimplementedError();
#endif
  }
}

navaronbracke avatar May 21 '22 19:05 navaronbracke

I'd like to know if the Dart team is at least considering this, especially for package maintainers, as we can't assume people using our packages will upgrade to the most recent version of Flutter or Dart.

A few things I have encountered recently that could be easily solved using conditional compilation, but require workarounds (or worse, maintaining multiple versions of the same library).

  • The addition of two methods to HttpClient in Dart 2.17 (required I ship two versions of my package, one that supports < 2.16 and one >= 2.17).
  • The change to make WidgetsBinding.instance non-optional
  • Upcoming changes to prefer using PlatformDispatcher.instance.onError over runZonedGuarded

fuzzybinary avatar Jul 18 '22 18:07 fuzzybinary

The more I think about this, the more I think what I'd really like here is API availability capabilities.

IOW, it's not that I really want preprocessor defines in Dart, it's that I want to know if some API is available (e.g. whether dart:ui as a whole is avialable, or whether dart:ui has a function called someNewAwesomeThing, or whether object X has a const constructor not, etc.).

dnfield avatar Jul 18 '22 18:07 dnfield

  • The change to make WidgetsBinding.instance non-optional

I figured out a way to circumvent this issue.

/// This allows a value of type T or T? to be treated as a value of type T?.
///
/// We use this so that APIs that have become non-nullable can still be used
/// with `!` and `?` on the stable branch.
// TODO: Remove this, once Flutter 2 support is dropped
// See https://github.com/flutter/flutter/issues/64830
T? _ambiguate<T>(T? value) => value;

Now you can do _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback(... without getting a warn.

martin-braun avatar Jul 19 '22 13:07 martin-braun

In Haxe language conditional compilation looks like this:

class Main {
  public static function main() {
    #if !debug
    trace("ok");
    #elseif (debug_level > 3)
    trace(3);
    #else
    trace("debug level too low");
    #end
  }
}

https://haxe.org/manual/lf-condition-compilation.html

VladimirCores avatar Jul 27 '22 11:07 VladimirCores

@dnfield

it's not that I really want preprocessor defines in Dart, it's that I want to know if some API is available (e.g. whether dart:ui as a whole is avialable, or whether dart:ui has a function called someNewAwesomeThing, or whether object X has a const constructor not, etc.).

But by what mechanics would you act on this knowledge? At the runtime all mentioned pieces can be known just by a generated consts – as Flutter installation comes with Dart sources.

ohir avatar Aug 08 '22 23:08 ohir

@ohir - there's no sane way currently to check at runtime whether a particular method (or method signature) is defined, and certain things are checked at compile time (like whether a const constructor is available or not).

I'm mainly thinking of how availability macros work in Objective-C on iOS/macOS. There I can do things like have code conditionally work if and only if I'm using a particular SDK version.

So for example, as a package author, I want to use some new exciting API that Flutter offers, but I have no way to check whether that API exists today (ignoring doing things like casting to dynamic and trying to invoke a method which is horrible for many reasons). I also don't have a way to have a single package offer better behavior depending on which version of the Flutter/Dart SDKs are in use. E.g.

  if (DartVersion > xxx) {
   // use cool new feature that is faster
  } else {
    // use slower fallback
  }

You can come close to getting this by having minimum version requirements in your pubspec, but pub has a hard time solving for strange and overlapping constraints - and it ends up requiring package maintainers to maintain multiple versions of their packages that may not even make it to their users correctly because of pub.

dnfield avatar Aug 08 '22 23:08 dnfield

I asked @munificent about this on Twitter and he mentioned that a general case pre-processor likely isn't coming any time soon, because it requires a lot of the tooling support for things like changing targets (think the target / platform selector on Visual Studio), and since we have Platform.isX, kDebugMode, kIsWeb and a few other compile time consts using Platform.environment, the one thing library creators / maintaners are lacking is what @dnfield is describing.

I feel like Dart could copy swift's available attributes and compiler directive for use with Dart and Flutter versions: https://www.avanderlee.com/swift/available-deprecated-renamed/

So similar to what Dan is suggesting we could have code like:

class MyHttpClient : HttpClient {
  @available(Dart >= '2.17')
  @override
  set connectionFactory(
          Future<ConnectionTask<Socket>> Function(
                  Uri url, String? proxyHost, int? proxyPort)?
              f) =>
      innerClient.connectionFactory = f;
}

Or:

if (#available(Flutter > 3)) {
  WidgetsBinding.instance.addObserver(this)
} else {
   WidgetsBinding?.instance.addObserver(this)
}

In both cases, the compiler should skip any code that doesn't meet the available criteria.

Since this mostly affects library maintainers, and we're likely to use fvm to change versions of Flutter, any support needed in tooling other than the analyzer could lag.

I'd be interested to hear what the Dart team thinks...

fuzzybinary avatar Aug 09 '22 15:08 fuzzybinary

how availability macros work in Objective-C on iOS/macOS

Similar functionality likely could be provided for Dart/Flutter own inventory, but for a substantial effort. I have no high hopes this will ever happen (ie. maintaining full lifecycles and supporting them thorough the toolchain).

pub has a hard time solving for strange and overlapping constraints

What about library author testing for all possible permutations (of execution path configurations) that are result of the pub solver coming to a given set of dependencies peculiar to the site of use? You can control, and that just to an extent, only your package's direct dependencies (version-wise).

I have no way to check whether that API exists today

I understand that you have no way to check whether that API exists at your package user's installation, am I correct? If I am, I know of no "contemporary automagic solution", but I think that 'ol pale ./configure might work (if contemporary users were used to do eg. a dart test configure step). There, in a first test, could be a place to do that ugly cast-try-catch research for API availability. Done once.

ohir avatar Aug 10 '22 09:08 ohir

Members! please add the conditional compilation feature to Dart just like C or Apple's Swift. It's important!

q25001560 avatar Oct 06 '22 13:10 q25001560

I am also missing C# pre-processor directives. Hope Dart will have this feature soon as well.

BradKwon avatar Oct 13 '22 09:10 BradKwon

We're missing this over here as well: https://github.com/gskinner/flutter_animate/issues/41

This is a bit downstream, but it would be quite nice to have the flutter versions eventually stored off as compilation consts:

#if flutter_3_3_1_or_newer
 //
#elif flutter_3_3_0
//
#else
 //
#endif 

So it would be cool if somehow that usecase was enabled (not just dart version).

esDotDev avatar Dec 05 '22 00:12 esDotDev

It would be nice to have kDebugMode as pre-processor variable as well. It would also be nice to have this ability in the pubspec.yaml somehow, or fix dev_dependencies already.

dev_dependencies apparently are shipped into the release build of Android (when imported in code, despite being used behind kDebugMode exclusively).

martin-braun avatar Aug 11 '23 01:08 martin-braun

dev_dependencies apparently are shipped into the release build of Android (when imported in code, despite being used behind kDebugMode exclusively).

If all uses are hidden behind if (kDebugMode) I would expect Dart code to be tree-shaken. Native dependencies will still be pulled in though.

Do you have an example?

mraleph avatar Aug 11 '23 06:08 mraleph

Native dependencies will still be pulled in though.

Yes, exactly this is my problem. Please see the backgrounds for my complaint here: https://github.com/Baseflow/flutter-geolocator/issues/1296#issuecomment-1674099396

The package in question that causes this problem though is safe_device, see https://github.com/ufukhawk/safe_device/issues/33

I only ever need the package in debug mode, so I assumed defining it in dev_dependencies would strip the native bits in production as well.

Apparently, the package causes a lot of problems in the Android release build that are massive and critical, but I still rely on the package during development, it makes things so much easier for me, because there are a lot of things that can only be tested on a real device.

Just for the record though: I switched to device_info_plus and my issue is solved, but the case was very real. For the sake of lightweight apps, dev_dependencies should be fully excluded from release builds, also the native bits.

Thanks.

martin-braun avatar Aug 11 '23 10:08 martin-braun

If all uses are hidden behind if (kDebugMode) I would expect Dart code to be tree-shaken. Native dependencies will still be pulled in though.

Why will native dev_dependencies be pulled? dev_dependencies assets are also shipped in the release build, which seems also wrong to me.

I see two important-to-fix bugs here, as there is no easy way for developers to work on development features and have the possibility to easily deactivate these features for release builds.

andynewman10 avatar Oct 04 '23 12:10 andynewman10