Dear everyone: Please stop using $IFDEF VERXXX

I just checked out the source to a new project.  Not going to name names because what I’m discussing is a pretty universal problem.  This project had dependencies on several common Delphi open-source libraries, and it had a well-designed DPROJ file that got all the paths right and everything.  I opened it in XE6 and went to build… and promptly tripped over some stupid compiler error involving ANSI vs Unicode chars… and by this point, anyone who’s ever done this probably knows exactly where this is going.

Turns out that this project included its own copy of various external libraries such as the JVCL, and on several of them, their $include files were not updated beyond XE3, and I had to locally patch every last one of them.  Aside from that (and one minor change to Indy that breaks things in XE5 and above but was trivial to fix) everything built just fine, but I just can’t help but wonder, why are we still doing this?  Even the main project itself had $IFDEF VERXXX checks, and it uses features not available before D2009, so there’s really no reason for it!

Well, I’m getting sick of it.  Can we finally put $IFDEF VERXXX out to pasture?  Using an $IF CompilerVersion check is superior in every way, especially forwards compatibility: if you say {$IF CompilerVersion >= X}, it won’t break when a newer version of Delphi comes out!  You won’t have to pointlessly update your $include file with every version for no reason other than to keep everything from breaking because a new version is out.  (Actually updating it for a good reason, that might happen, but it would be a much simpler process too, with a lot less pointless repetition required.)

And yes, I’m aware that in some really ancient versions of Delphi, the $IF directive did not exist, and yes, I’m aware that a few people are still using those.  If it’s really that important to keep supporting those versions, putting it inside a few nested $IFNDEF checks to rule out ancient versions would do the trick just fine.  But especially when for creating new projects that rely on language features that didn’t exist back then, there’s really no excuse to continue to use the old, broken way of checking the language version.  About the only actually valid use for it I’ve seen is to test for exactly what it says: Is the version exactly equal to this version, because there are bugs in that specific compiler that I need to work around in that case?

That’s a valid use, but can we please stop abusing it for feature testing?

23 Comments

  1. Anthony FRazier says:

    Yes please! If you have trouble with the various version mappings (I lose track sometimes), just grab jedi.inc and use that. It’s so much easier than rolling your own.

    • Everyone should just use the jedi.inc – countless times I had to update inc files that used some DelphiXXX_UP directive that still was not forwards compatible.

      • Eric says:

        The problem is exactly because of that, there is not one but many Jedi.inc.

        The other problem is using version numbers in the compiler isn’t meaningful hence the need for _UP defines that are easily understood, and that use of meaningless compiler versions in the compiler is probably the real root of all evil.

        Jedi.inc shouldn’t have been necessary after a few Delphi versions, it should (or something similar) have been integrated in the compiler.

      • Qian Xu says:

        It is meaningless to create ton of DELPHI_XX_UP. If your code suddenly has a {$ifdef DELPHI5_UP}, who knows what the hell is that for.

        More practical is to have feature based directives, such as {$ifdef UNICODE}, {$ifdef FOO_FEATURE}. We’d indeed start a `delphi-inc` project.

  2. Serg says:

    The standard *.inc file template is:

    [code]
    {$IFDEF CONDITIONALEXPRESSIONS}
    {$IF CompilerVersion >= 20.0} // Delphi 2009
    // ..
    {$IFEND}
    {$IF CompilerVersion >= 21.0} // Delphi 2010
    // ..
    {$IFEND}
    // .. newer versions
    {$ELSE}
    // .. use $IFDEF VERXXX for legacy version checks if you still support them
    {$ENDIF}
    [/code]

    • himitsu says:

      {$IFDEF CONDITIONALEXPRESSIONS} you can actually leave out but since this feature is supported for over 12 years.

      I use the {$IF CompilerVersion >= 20} also been around for many years. (since Delphi 7)
      Otherwise you would have to become his semi-annually adjust the IFDEFs.

      And testing for a range of practical anyway.
      {$IF (CompilerVersion >= 20) and (CompilerVersion < 23)} // 2000 to XE

      • Thaddy says:

        No, you can’t.

        You will run into trouble with the $IF/$IFEND statements while maintaining for D2-D5 and/or FPC versioning directly (which I do, and frankly many still do). And Freepascal doesn’t know about the CONDITIONALEXPRESSIONS define, although it supports Conditional Expressions It doesn’t know abouit the CompilerVersion and RTLVersion constants, so using that will break iof not caught beforehand.

        But anyway. It is possible to limit VERXXX to an absolute minimum.
        For Freepascal you can use {$IF FPC_FULLVERSION >= 2.0}..{$IFEND}

        • Remy Lebeau says:

          I agree with this. Indy deals with Delphi 5 and FreePascal, so it does make things a bit more difficult. And even for Delphi, when {$IF} was first added in Delphi 6, there were issues for awhile with mixing {$IF} and {$IFDEF/IFNDEF} together. And to top it off, Embarcadero recently deprecated {$IFEND} in favor of {$ENDIF} so you have to deal with {$LEGACYIFEND} as well.

    • Mason Wheeler says:

      Wow. I didn’t know there was a {$IFDEF CONDITIONALEXPRESSIONS}. That’s useful! 🙂

    • Remy Lebeau says:

      {$IF} and CONDITIONALEXPRESSIONS were introduced in Delphi 6.

  3. PhiS says:

    More trouble still when a project needs to compile under Delphi and FPC…

    • himitsu says:

      It would be better anyway, if you are not tested for compiler version, but possible on the feature.

      {$IFDEF UNICODE} instead of {$IF CompilerVersion >= 20.0} // Delphi 2009

      • Remy Lebeau says:

        Yes, you should write your code to test for features, not versions. But {$IFDEF UNICODE} is a special case. Most compiler/RTL features do not have their own conditionals, you have to define your own based on versions. And FreePascal does not define UNICODE by default as it still uses Ansi strings unless you enable Unicode strings.

  4. Kmorwath says:

    “Turns out that this project included its own copy of various external libraries”
    That’s the original sin. Unless you document somewhere it needs to be built exactly with a given Delphi version because of it.
    Any project carrying around outdated library has a good risk to break when build on a newer release of Delphi – and even if it compiles – it will run on libraries that were never tested with the compiler you’re using.

  5. Remy Lebeau says:

    What “minor change to Indy that breaks things in XE5 and above” needed to be fixed?

    • Mason Wheeler says:

      Specifically, the following call:

      FIndyWS.IOHandler.Write(s, TEncoding.UTF8);

      where FIndyWS is defined as TIdHTTPWebsocketClient. This will compile in XE4 and below, and break in XE5, where it has to be replaced by:

      FIndyWS.IOHandler.Write(s, IndyTextEncoding(TEncoding.UTF8));

      • Remy Lebeau says:

        You should not be using TEncoding directly with Indy to begin with, especially for standard encodings like UTF-8. Use Indy’s built-in encoding framework instead. That statement should have been originally written as “FIndyWS.IOHandler.Write(s, TIdTextEncoding.UTF8);” instead. In the latest version, it should now be written as “FIndyWS.IOHandler.Write(s, IndyTextEncoding_UTF8);”. Indy has never used TEncoding directly (although TIdTextEncoding was defined as just an alias for TEncoding in Delphi 2009+, that was an implementation detail you should not have relied on). TIdTextEncoding was replaced with IIdTextEncoding in Indy 10.6 (see http://indyproject.org/sockets/blogs/ChangeLog/20130423.EN.aspx), which shipped with XE4, not XE5. Moving forward, if you need to support earlier Indy 10 versions, a version-neutral statement should be written as “FIndyWS.IOHandler.Write(s, IndyUTF8Encoding);” or “FIndyWS.IOHandler.Write(s, enUTF8);”. The IndyUTF8Encoding() and enUTF8() wrappers (although marked as deprecated) have always mapped to the correct implementation.

  6. @Kmorwath:

    > “Turns out that this project included its own copy of various external libraries”.
    > That’s the original sin.

    I disagree with you on this.

    It would cause a total mess if you blindly updated all your external dependencies to the svn-head of the external project.

    First, you would get into trouble during development and release. Imagine that your code works all fine – with the external library is on a specific revision. You commit the code, but during the night the external library submits several updates to their svn. So, the next day your build server will run svn-update, and the fixes in the external libraries will be pulled put. Your server will now build the project with an environment you have never worked with yourself. If the build fails, you would be dragged into a bug hunting you weren’t prepared for. If the build succeeds, you have no idea whether the fixes in the external project could lead to runtime problems.

    Second. If you revert your project to a specific svn revision (or switch to a branch), you would also like the external library to be reverted to the state it had at that revision. If you work with v2.0 in the trunk and you get a bug report for the released v1.5, you would certainly like to debug the code with the correct version of the external project. It would indeed be risky to release even a simple fix for v1.5 if all your external project got randomly updated as well.

    This might sound trivial, but it’s not.
    All the external svn-projects have different “personalities”. Some are very strict on svn-head quality, and some are very experimental. I’ve experienced more than once that an external project must stay a few revisions behind the head to work properly.

    When you have several external projects involved, YOU have to be in charge of the state of each external project you use in you own project.

    If you need (or want) to update a third party library, the update should be treated as a dedicated task. Just like a new feature, a bug fix or a source code refactoring. Preferably registered in your bug tracker, but at least committed to the svn as a specific revision.

    This is not as cumbersome as it might sound. In the root folder of your project, you specify the external projects you use and the revision you want to use (under svn properties). When you check out your project, the external libraries will be updated according to these properties. Any changes in the svn properties must be committed to svn, so you will always have a complete overview over which revision your external libraries had at any given point.

    Now, the cool thing is that the directories with the external libraries behave as individual svn projects. Let’s say you collect all your external projects in the folder “externals-svn”, then you can just navigate to “.\externals-svn\[AnyProject]\” and perform a svn-update of [AnyProject] directly. [AnyProject] will now be updated to HEAD, and you can check if everything compiles and behaves properly. If something goes wrong – fix it or leave it for later. If everything looks OK, update the svn properties in the root folder and commit.

  7. Farid Shmidt says:

    if you use $IFDEF VERXXX or the jedi.inc then it’s kind of you, for those who won’t u$p$g$r$a$d$e anymore.

    • Thaddy says:

      Our K$O$L$ $l$l$i$b$r$a$r$y had unicode support way before Delphi. Although I hardly use KOL for commercial stuff, it happens.

      Why would you upgrade a stable compiler (d5, d7, fpc 2.6.4) and budget for bugfixing? And pay for bug fix releases? Not my customers!

      And, not to put too fine a point to it: Freeze the versioned compiler(s) with the project. Easy tip that not many seem to realize the point it makes.

      I just had a case where my own d2006 e/a disks were damaged beyond repair and I nearly had to budget for upgrades instead of just adding a simple feature for a customer with a d2006 codebase. And in that case somebody didn’t do what I just wrote and took the disks home. (Payed for by the customer)

  8. Andreas Dorn says:

    Dear everyone: Please stop using crappy Direktives.

    1) $IF CompilerVersion
    This one is highly compiler-specific. If you code for magical variables of special compilers
    you’re doing it wrong.

    2) $IF Declared()
    I found about 20 places where someone actually used this. Mainly for a kludge about
    copyright notices and the GPL and something exotic about Kylix-Versions.
    That statement is only useful for creating Direktive-spaghetti.

    All in all I think around D6 something went wrong with the compiler-directives and back then
    the compiler-developers shamelessly added a lot of garbage to the compiler.

Leave a Reply to Serg