Blog

All Blog Posts  |  Next Post  |  Previous Post

Object Pascal: Compiler Directives

Tuesday, June 26, 2018

Photo by Mathyas Kurmann on Unsplash

Compilation Directives could help you to make your code multi-platform or even cross-compiled.

Introduction

Compilation directives are powerful commands that developers could use to customize how the compiler works.

These directives pass parameters to the compiler, stating the arguments of the compilation, how must be compiled, and which will be compiled.

There are basically 3 types of compilation directives.

  • Switch directive
  • Parameter directive
  • Conditional compilation directive

The first two types change the compile parameters, while the last one changes what the compiler will perform on.

In this article we will deal with the last one: Conditionals.

Conditionals

They are powerful commands.

With just a few conditional commands, your Object Pascal code can be compilable across multiple platforms.

However, as we add more and more directives, the code will become more complex.

Let's take a look in the example below:

 //http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Conditional_compilation_(Delphi)
  {$DEFINE DEBUG}
  {$IFDEF DEBUG}
  Writeln('Debug is on.'); // This code executes.
  {$ELSE}
  Writeln('Debug is off.'); // This code does not execute.
  {$ENDIF}
  {$UNDEF DEBUG}
  {$IFNDEF DEBUG}
  Writeln('Debug is off.'); // This code executes.
  {$ENDIF}

In this example, only the first and third Writeln function calls will be executed.

All directives and also the second function call won't be part of the final executable, I mean, the ASSEMBLY code.

Cool.

However, it looks like that the code is "dirty" and also we have a temporal coupling, because the constants need to be defined in a specific order.

Directives and definitions of constants that will be used in only a single unit may even be manageable, but what if you will work with tens or even hundreds of units that will use these directives and definitions, do you still think this approach is the best choice for the architecture of your project, with the purpose of building it as cross-compiled or multi-platform?

I don't think so.

Encapsulating Directives

Imagine a project that needs to be cross-compiled in Delphi and Free Pascal. We would like to use the same classes, the same code, but could exists some differences between these compilers.

The code needs to evolve independent of the compiler. I mean, if some changes could be done to improve the code when it is compiled on Free Pascal, for example, it should be done without thinking in some difference that might exist in Delphi.

To do so properly, we could not work in a code with many compilation directives, because some changes could broke the Delphi version or vice-versa.

Instead of seeing directives, it might be better seeing just classes.

I would called this, encapsulated directives.

Implementation

Imagine an unit witch contains a class to represents a MD5 algorithm.

Free Pascal already has a md5 unit which has functions to do the job and of course we have to make classes to encapsulate those functions.

In Delphi, the unit that do the same job is named hash.

We do not want to "reinvent the wheel" then, let's use what is already done on both platforms.

So, how would you do this implementation neither using conditional directives in implementation code nor *.inc files?

First of all, let's create our MD5 unit:

unit MD5.Classes;

interface

uses
  {$ifdef FPC} MD5.FPC {$else} MD5.Delphi {$endif};

type
  TMD5Encoder = class(TEncoder);

implementation

end.

As you can see, there is almost nothing on this unit. The real code will stay in others units, i.e, MD5.FPC and MD5.Delphi, one for each platform.

Let's create the FPC version:

unit MD5.FPC;

interface

uses
  md5;

type
  TEncoder = class
  public
    function Encode(const Value: string): string;
  end;

implementation

function TEncoder.Encode(const Value: string): string;
begin
  Result := MD5Print(MD5String(Value));
end;

end.
Those functions with prefix "MD5" comes from md5 unit.

Then, let's create the Delphi version:

unit MD5.Delphi;

interface

uses
  hash;

type
  TEncoder = class
  public
    function Encode(const Value: string): string;
  end;

implementation

function TEncoder.Encode(const Value: string): string;
begin
  Result :=THashMD5.GetHashString(Value);
end;

end.

Both units have the TEncoder class definition (yes, same name in both). Then, we created a TMD5Encoder class that inherits from the correct class, which is platform dependent, and voila! We have clean units, without any conditional directives inside methods.

Finally, we have two distinct classes in different units that can evolve its implementation independently, without any fear of breaking the code between platforms.

Conclusion

Compilation directives is a good tool for customize the code. However, it should be used with parsimony.

In object-oriented code, try to use more specialized objects than compilation directives.

For each conditional directive that you want to add to the code, I suggest implementing a new class that encapsulates the directive or a set of them.

Your code will be cleaner and sustainable.

See you.



Marcos Douglas B. Santos




This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post