Saturday, January 23, 2021

The IFDEF Problem!


If you are using {$IFDEF} in your source - normally it's fine...

e.G.

{$IFDEF DEBUG}
ShowMessage('Foo');
{$ENDIF}


Why, because this DEBUG is handled by the IDE.

But what if you want to use your own?

{$IFDEF IMPORTANT}
Result := 42;
{$ENDIF}

If your application depends on this, you always have to do an else part to make sure the Name "IMPORTANT" is set.

{$IFDEF IMPORTANT}
Result := 42;
{$ELSE}
! Too stupid set this!
{$ENDIF}

I don't want to have this in my source in every place I just want to exclude or include something.

Like:

{$IFDEF UseLogger}
Log('Ready to run');
{$ENDIF}

In this case, I want to be able to include logging, but on the other hand, I want to be sure that I disabled logging for a good reason, not just forgot to set the {$DEFINE UseLogger}.

That's why I like to use const's:

Unit Defines;

Interface
  Const
    UseLogger = true;
Implementation

end.

{$IF Uselogger} is also not working if you just forgot to "uses" your Defines.pas Unit.

That's why I'm using the not implementation.

Unit Defines;

Interface
  Const
    NotUseLogging = false; // means use logger
Implementation

end.

Because using:

{$IF not(NotUseLogging)} has the benefit of checking if there is a symbol named NotUseLogging!

So if you forgot to use the Defines Unit you are getting an error at compile time. I'm using the approach for my FDK and also for my #D.MVVM Framework.

This works fine... But there is one drawback: You are unable to change these settings per project. In the past, I've changed the source of my defines unit, but changing this back and forth is a pain. Of course, this unit is named Delphiprofi.FDK.IFDEF.pas!

The other drawback is, you are unable to use the {$UNDEF Foo} in a Unit. So what is the best solution?

Let's collect the necessary things we want to accomplish:

We want to be sure to set important things globally for the whole framework. That's why I use the {$IF Not(Name)} statement. In the past, we all used this stupid {$I defines.inc} approach, but I hate that so much and it also has the problem that your {$IFDEF Foo} is unable to check if you just forgot to include the file. How often have you searched for a problem, because you misspelled a conditional define? I always type  {$IFDEF WINDOWS} instead of   {$IFDEF MSWINDOWS}.
Of course, we can still use normal {$DEFINE Foo} and {$IFDEF Foo} for local unit settings during development. While using the "if not trick" in every unit we are sure that all {$IF not} statements have the right setting... 
And how to use these settings on a project level? Of Course with a normal {$IFDEF} in the Delphiprofi.FDK.IFDEF.pas Unit. In my case, I take this idea one step further by using a class like:

ExcludeFDK = class sealed 
  public
    const
     Logging     = false;
     //... more in the original source
end;

With this, I just can type ExcludeFDK "." and the IDE gives me all possible settings (with the right spelling).

The If looks then like this:

{$IF Not(ExcludeFDK.Logging)}
FDKConfig.GetLogger( Self ).Warning( 'This (%s) should not be happend',[aMSG]);
{$ENDIF}

btw. The FDK is also able to disable logging, based on the Type of the Class the logger is fired from. If I want to disable all the logging from my DI - Container, I just set the exclude of this class in code. Very handy!

To be able to include or exclude - in this example - the logging from a project I can use normal defines on the project level. In this case, it is safe, because I know that all units are set correctly. This it how it looks:

ExcludeFDK = class sealed 
  public
    const
     {$IFNDEF FDKNoLogging}
     Logging     = false;
     {$ELSE}
     Logging     = true;
     {$ENDIF}
     //... more in the original source
end;

If I have no conditional defines in my project settings, the behavior is the default. But by using FDKNoLogging all the logging functionality is not linked to the project.

But this is only necessary if I want to exclude the logging completely from a project to reduce the size. If I keep the logging linked, but do not set a logging target, a NIL logger (empty procedure call) is called with the least possible CPU cycles. So you are able to activate logging - for example - over a command line param.

If you want more information about the logging features like Log to Console, Debug Window, Testinsight, SmartInspect, or to a remote server or any of the other targets, please leave a comment.

That's it for the moment... More is coming...

No comments:

Post a Comment