Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional Compilation in Perl [duplicate]

How do I get the following code to work?

use strict;
use warnings;

if ($^O eq 'MSWin32' || $^O eq 'MSWin64') {
    use Win32;    
    Win32::MsgBox("Aloha!", MB_ICONINFORMATION, 'Win32 Msgbox');     
}
else {
    print "Do not know how to do msgbox under UNIX!\n";
}

The above runs under Windows. But under UNIX, there is a compilation error as Win32 cannot be found. Replacing "use" with "require" makes things worse -- the code would fail to compile under both Windows and UNIX because the line containing MB_ICONINFORMATION is always compiled and "MB_ICONINFORMATION" would be an undeclared bare-word.

So how do I get around this problem?

like image 894
Shang Zhang Avatar asked Jan 03 '23 07:01

Shang Zhang


1 Answers

Perl compiles code first to an intermediate representation, then executes it. Since the if is evaluated at runtime but the use is handled during compilation, you are not importing the module conditionally.

To fix this, there are a number of possible strategies:

  • conditional import with the use if pragma
  • conditional import with a BEGIN block
  • require the module
  • defer compilation with eval

To import a module only when a certain condition is met, you can use the if pragma:

use if $^O eq 'MSWin32', 'Win32';

You can also run code during compilation by putting it into a BEGIN block:

BEGIN {
  if ($^O eq 'MSWin32') {
    require Win32;
    Win32->import;  # probably not necessary
  }
}

That BEGIN block behaves exactly the same like the above use if.

Note that we have to use require here. With a use Win32, the module would have been loaded during the compile time of the begin block, which bypasses the if. With require the module is loaded during runtime of the begin block, which is during compile time of the surrounding code.

In both these cases, the Win32 module will only be imported under Windows. That leaves the MB_ICONINFORMATION constant undefined on non-Windows systems. In this kind of code, it is better to not import any symbols. Instead, use the fully qualified name for everything and use parentheses for a function call (here: Win32::MB_ICONINFORMATION()). With that change, just using a require instead of an use if may also work.

If you need code to be run later, you can use a string-eval. However, this potentially leads to security issues, is more difficult to debug, and is often slower. For example, you could do:

if ($^O eq 'MSWin32') {
    eval q{
        use Win32;    
        Win32::MsgBox("Aloha!", MB_ICONINFORMATION, 'Win32 Msgbox');     
        1;
    } or die $@;  # forward any errors
}
  • Because eval silences any errors by default, you must check success and possibly rethrow the exception. The 1 statement makes sure that the eval'ed code returns a true value if successful. eval returns undef if an error occurs. The $@ variable holds the last error.
  • q{...} is alternative quoting construct. Aside from the curly braces as string delimiters it is exactly the same as '...' (single quotes).

If you have a lot of code that only works on a certain platform, using the above strategies for each snippet is tedious. Instead, create a module for each platform. E.g.:

Local/MyWindowsStuff.pm:

package Local::MyWindowsStuff;
use strict;
use warnings;
use Win32;

sub show_message {
  my ($class, $title, $contents) = @_;
  Win32::MsgBox("Aloha!", MB_ICONINFORMATION, 'Win32 Msgbox');
}

1;

Local/MyPosixStuff.pm:

package Local::MyPosixStuff;
use strict;
use warnings;

sub show_message {
  warn "messagebox only supported on Windows";
}

1;

Here I've written them to be usable as classes. We can then conditionally load one of these classes:

sub load_stuff {
  if ($^O eq 'MSWin32') {
    require Local::MyWindowsStuff;
    return 'Local::MyWindowsStuff';
  }
  require Local::MyPosixStuff;
  return 'Local::MyPosixStuff';
}

my $stuff = load_stuff();

Finally, instead of putting a conditional into your code, we invoke the method on the loaded class:

$stuff->show_message('Aloha!', 'Win32 Msgox');

If you don't want to create extra packages, one strategy is to eval a code ref:

sub _eval_or_throw { my ($code) = @_; return eval "$code; 1" or die $@ }

my $show_message =
  ($^O eq 'MSWin32') ? _eval_or_throw q{
    use Win32;
    sub {
      Win32::MsgBox("Aloha!", MB_ICONINFORMATION, 'Win32 Msgbox');
    }
  } : _eval_or_throw q{
    sub {
      warn "messagebox only supported on Windows";
    }
  };

Then: $show_message->() to invoke this code. This avoids repeatedly compiling the same code with eval. Of course that only matters when this code is run more than once per script, e.g. inside a loop or in a subroutine.

like image 140
amon Avatar answered Jan 13 '23 12:01

amon