Search code examples
perlserialization

Passing object between programs running under different perl versions


Facing an issue while passing object as an input parameter with different perl versions from perl5.6.pl to perl5.24.pl (unable to get the return value from function "$from_5.24"). Provided below the code which has the problem. Using windows platform, how to solve this issue.

SharedBetweenPerls.pm:

package SharedBetweenPerls;
use warnings;
use strict;
use Exporter;
our @ISA = 'Exporter';
our @EXPORT_OK = qw(getVal);

sub new {
   my $self = {
      roleId => undef,
      username => undef,
   };
   bless $self, 'SharedBetweenPerls';
   return $self;
}

sub getVal{
  my ($self) = @_;
  return $self->{'roleId'};
}
1;

v5.24.pl:

use warnings;
use strict;
use v5.24;
use lib '.';
my ($self) = @_;
print $self->{'roleId'}; # Not working

v5.6.pl:

use warnings;
use strict;
use lib '.'; 
use SharedBetweenPerls;

my $obj =  new SharedBetweenPerls();
$obj->{'roleId'} = '10000';
$obj->{'username'} = 'test123';

my $roleId = $obj->getVal();
print "Value : $roleId \n"; # working fine

my $from_5.24 = qx(path-to-perl-5.24 program_for_5.24.pl "$obj"); 
print "Return from function: $from_5.24"; # Not Working

I have tried using serialize test code (SharedBetweenPerls.pm) given by zdim. I have got following malformed error.

malformed JSON string, neither tag, array, object, number, string or atom, at character offset 0 (before "roleId") at SharedBetweenPerls.pm

5.6.pl:

use warnings;
use strict;
use lib '.'; 
use SharedBetweenPerls;
use IPC::System::Simple qw(capturex);
#my $data = '{"roleId":31, "username":"test123"}';
#my $obj = SharedBetweenPerls->new($data);
my $obj = SharedBetweenPerls->new(roleId => 17, username => 'test123');
my $result = capturex('D:\Perl\bin\perl524.exe', 'D:\sample_program\p5.24.pl', '$obj->serialize');
print "return from function: $result";

5.24.pl:

use warnings;
use strict;
use v5.24;
use lib '.';
use SharedBetweenPerls;
my ($init_json) = @ARGV;
my $obj = SharedBetweenPerls->new( $init_json );
print $obj->{'roleId'};

Solution

  • Note   There are two serialization approaches below -- using Storable (may be hit or miss given that it need be between different Perl and thus module versions), and a custom one


    This is a more general case of the problem posed in the previous question, where now an object is to be passed between programs. This change brings about a substantial difference.

    An object must be serialized in order to be passed around, so that we can pass it byte by byte down a pipe of some sort. I use Storable for that purpose in this demo, but you may need to look for other tools or perhaps write a custom process (demonstrated in the added part at the end)

    With some other adjustments, to be discussed below, here are the files.

    The package SharedBetweenPerls.pm

    package SharedBetweenPerls;    
    use warnings;
    use strict;
    
    sub new {
        my ($class, %args) = @_; 
        my $self = { %args };
        return bless $self, $class;
    }
    
    sub get_roleId {
        my ($self) = @_; 
        return $self->{'roleId'};
    }
    
    1;
    

    The program that needs to run under v5.24 (v.5.24.pl)

    use warnings;
    use strict;
    
    use Storable qw(retrieve);
    
    use v5.24;    
    
    use FindBin qw($RealBin); 
    use lib $RealBin;       # look for modules in this script's directory
    use SharedBetweenPerls;
    
    my ($file) = @ARGV;
    
    my $obj = retrieve($file) // warn "There were errors: $!";
    
    print $obj->get_roleId;
    

    The "main" program, that must run under an older perl

    use warnings;
    use strict;
    use feature 'say';
    
    use Storable qw(store);
    
    use FindBin qw($RealBin); 
    use lib $RealBin;   
    use SharedBetweenPerls;
    
    my $obj = SharedBetweenPerls->new(roleId => 17, username => 'test123');
    
    my $roleId = $obj->get_roleId();
    say "Value for 'roleId' in the new object: $roleId";
    
    my $outfile = "obj_$$.storable";  # store the serialized object in a file
    store($obj, $outfile) // warn "There were errors: $!";                 #/
    
    # (replace my perlbrew path with your actual path to the v5.24 executable)
    my $perl_524 = qq($ENV{HOME}/perl5/perlbrew/perls/perl-5.30.0/bin/perl);
    my $cmd = qq($perl_524 v5.24.pl $outfile);
    my $from_524 = qx( $cmd );
    chomp $from_524;    
    say "Return from function: $from_524";
    
    unlink $outfile or warn "Can't unlink $outfile: $!";  # can delete file now
    

    The file to which the serialized object is written should have a far better name than what I use in this demo, and File::Temp is the standard choice for handling temporary names.

    This prints

    Value for 'roleId' in the new object: 17
    Return from function: 17
    

    So for this simple toy-class this works -- the object gets passed correctly.

    However, serialization is by no means a trivial affair. Depending on how complex your actual class is and, in particular, how different the module versions for two programs are, there may be problems. With your combo of v5.6 and v5.24 I think you need to keep fingers crossed. (It worked with my v5.16 and v5.30 but v5.6 is very, very old.)

    The combination store+retrieve is (one way for) how complex data is passed using files. I also tried to freeze the object and manually write it to a file and then read that file in and thaw it, and that worked as well. (Passing the frozen object directly down the pipe is troubled.)

    But passing a whole object may just not work, and if there are indeed problems in your case then what to do would depend entirely on what your class is actually like.

    One thing you can always do is to come up with a custom serialization approach, whereby needed data is passed along (via a file or suitably serialized for a pipe), not the whole object. Then the program on the other end can use that to construct the object.

    If data is passed using files, what is a clear option, then we are talking about persistence.

    Comments

    • When a package defines a class there is no reason to export symbols

    • Don't hard-code the package name; there is __PACKAGE__ for that. But with a class the package name gets passed as the first argument in the constructor, and that should be used

    • Don't use an object as any old hashref, where one simply dereferences a key to print a value. That pokes at a class internals and is a really, really bad idea -- use provided methods. (For that the v.5.24.pl program needs to load the package with the class, too)

    • If you want the called program to be able to work with an object it should load the package where that class is defined (since one shouldn't use the object as a mere hashref)

    • The indirect method notation (new ClassName) is great to avoid. Use a normal method call instead (ClassName->new). For one thing, the constructor is a method

    • A program's arguments are in @ARGV, not in @_

    • The class above needs a whole lot more but that would take us elsewhere

    • I recommend using a module to run external commands, not backticks (qx). Try some of: IPC::System::Simple, Capture::Tiny, IPC::Run3, IPC::Run


    An example

    Add a method to your class that implements the needed de/serialization. It may simply create a hash with all attributes and their values, and then serialize it -- for example generate a JSON string out of it. (If the values of some attributes are objects of yet other classes then you'd have to do more work.)

    Then the v5.6.pl program can do (using IPC::System::Simple module here)

    use IPC::System::Simple qw(capturex);
    ...
    my $from_524 = capturex($perl_524, 'v5.24.pl', $obj->serialize);
    

    and the v.5.24.pl program can then do

    my ($init_json) = @ARGV;
    
    my $obj = SharedBetweenPerls->new( $init_json );
    

    So now the target program has an object built with needed data and can get to work.

    Here is a very basic and crude example. Please note that it may be a poor choice for your project, which I know nothing of, while if it can be used then it needs more work/checks.

    I use JSON to serialize the hash of attributes and their values; that is done in the serialize method. Then such a JSON string can be used to construct a new object: The constructor checks whether it got a hashref or a string, and for a string it calls a method (init_from_json) which initializes the object.

    sub new {                              # WARNING: a sketchy demo only
        my ($class, $args) = @_; 
        my $self = {}; 
        bless $self, $class;
        my $ref = ref $args;
        if (not $ref) {                    # a string; better be JSON
            $self->init_from_json($args);
        }   
        elsif ($ref eq 'HASH') {           # straight-up attributes, initialize
            $self->{$_} = $args->{$_}  for keys %$args;
        }   
        else { croak "Unsupported invocation..." }  # print user message etc
        return $self;
    }
    
    sub serialize {
       my $self = shift;
        require JSON; JSON->import('encode_json');
        my %attr = map { $_ => $self->{$_} } keys %$self;
        return encode_json(\%attr);  # (no objects please)
    }
    
    sub init_from_json {
        my ($self, $args_json) = @_; 
        require JSON; JSON->import('decode_json');
        my $args = decode_json($args_json);
        $self->{$_} = $args->{$_} for keys %$args;
    }
    
    ...
    

    Now the v5.6.pl program can create its object and serialize it, then invoke the v5.30.pl program with that JSON string passed to it as input. The v5.30.pl program can then rebuild the object from JSON and do its work with it.

    There are many other ways to do this, depending on specifics.

    If you were to use a framework for OO code, like Moose or Moo, then there are ready tools and techniques which would help. (There'd also be a some learning curve then, if you haven't used these frameworks.)


    As asked for, complete working programs (maximally simplified to help debugging)

    v5.6.pl

    use warnings;
    use strict;
        
    use IPC::System::Simple qw(capturex);
    
    use FindBin qw($RealBin); 
    use lib $RealBin;   
    use SharedBetweenPerls;
    
    my $obj = SharedBetweenPerls->new( { roleId => 17, data => [3..7] } );
    
    # (replace my perlbrew path with your actual path to the v5.24 executable)
    my $perl_524 = qq($ENV{HOME}/perl5/perlbrew/perls/perl-5.30.0/bin/perl);
    
    my $from_524 = capturex( $perl_524, 'v5.30.pl', $obj->serialize );
    print "Return from function: $from_524";
    

    v5.30.pl

    use warnings;
    use strict;
    
    use v5.24;
    use FindBin qw($RealBin); 
    use lib $RealBin;   
    use SharedBetweenPerls;
    
    my ($init_json) = @ARGV;
    
    my $obj = SharedBetweenPerls->new($init_json);
    print $obj->get_roleId;