32

This is duplicate of my question on SWIG mailing list.

I am trying to use stl containers in my SWIG bindings. Everything works perfectly except for stl map handling in Perl. On C++ side, I have

std::map<std::string, std::string> TryMap(const std::map<std::string, std::string> &map) {
  std::map<std::string, std::string> modified(map);
  modified["7"] = "!";
  return modified;
}

SWIG config look like this

%module stl

%include "std_string.i"

%include "std_map.i"
%template(StringStringMap) std::map<std::string, std::string>;

%{
  #include "stl.h"
%}

%include "stl.h"

In my Python script I can call TryMap this way

print dict(stl.TryMap({'a': '4'}))

and get beautiful output

{'a': '4', '7': '!'}

but in Perl I call

print Dumper stl::TryMap({'a' => '4'});

and get an error

TypeError in method 'TryMap', argument 1 of type 'std::map< std::string,std::string > const &' at perl.pl line 7.

I can actually do something like

my $map = stl::TryMap(stl::StringStringMap->new());
print $map->get('7');

and get '!', but this is not an option because there is a lot of legacy code using "TryMap" that expects normal Perl hash as its output.

I believe there is a way work this out because SWIG solves this particular problem nicely in Python and even in Perl if I use stl vectors and strings but not maps.

Is there any way to handle stl map with Perl in SWIG? I am using latest SWIG 2.0.7

UPDATE Maybe there is something wrong with perl5/std_map.i. It is too short =)

$ wc -l perl5/std_map.i python/std_map.i 
   74 perl5/std_map.i
  305 python/std_map.i
quantum
  • 3,672
  • 29
  • 51
alexanderkuk
  • 1,551
  • 2
  • 12
  • 29
  • 1
    Your Perl code is passing a reference to a hash. I do not know SWIG at all, but I think that is important. What happens if you change the C++ side to expect a pointer? – Sinan Ünür Jun 22 '12 at 14:02
  • I have TryVector that is analogous to TryMap (expects reference), I use it like 'print Dumper stl::TryVector([1, 2]);' and everything works fine – alexanderkuk Jun 22 '12 at 14:14
  • 2
    I just noticed a typo: **`stl::TryMap({'a' => '4});`** Does your original code have that? – Sinan Ünür Jun 22 '12 at 16:14
  • 1
    Q: Would you rather have efficient or intuitive? It's possible to write something that would do what you wanted, but it means creating, populating and returning a new hash for every call, whereas it is possible to make `->{'key'}` syntax work without lots of copies and still accept hashes as input too. (I think it's possible to customise how `Data::Dumper` works if you care about it specifically, but I've not done that before). – Flexo Jun 26 '12 at 08:46
  • Have you tried to remove **const** for the function parameter in C++? – Alex Cohn Sep 12 '12 at 21:27

1 Answers1

2

I put your C++ function into header file as an inline function for testing.

I was then able to construct a SWIG interface that does what you are looking for. It has two key parts. Firstly I wrote a typemap that will allow either a std::map, or a perl hash to be given as input to C++ functions that expect a std::map. In the case of the latter it builds a temporary map from the perl hash to use as the argument. (Which is convenient but potentially slow). The typemap picks the correct behaviour by checking what it was actually passed in.

The second part of the solution is to map some of the C++ map's member functions onto the special functions that perl uses for overloading operations on hashes. Most of these are implemented simply with %rename where the C++ function and perl functions are compatible however FIRSTKEY and NEXTKEY don't map well onto C++'s iterators, so these were implemented using %extend and (internally) another std::map to store the iteration state of the maps we're wrapping.

There are no special typemaps implemented here for returning the maps, however there is extra behaviour via the special operations that are now implemented.

The SWIG interface looks like:

%module stl

%include <std_string.i>
%include <exception.i>

%rename(FETCH) std::map<std::string, std::string>::get;
%rename(STORE) std::map<std::string, std::string>::set;
%rename(EXISTS) std::map<std::string, std::string>::has_key;
%rename(DELETE) std::map<std::string, std::string>::del;
%rename(SCALAR) std::map<std::string, std::string>::size;
%rename(CLEAR) std::map<std::string, std::string>::clear;

%{
#include <map>
#include <string>
// For iteration support, will leak if iteration stops before the end ever.
static std::map<void*, std::map<std::string, std::string>::const_iterator> iterstate;

const char *current(std::map<std::string, std::string>& map) {
  std::map<void*, std::map<std::string, std::string>::const_iterator>::iterator it = iterstate.find(&map);
  if (it != iterstate.end() && map.end() == it->second) {
    // clean up entry in the global map
    iterstate.erase(it);
    it = iterstate.end();
  }
  if (it == iterstate.end())
    return NULL;
  else
    return it->second->first.c_str();
}
%}

%extend std::map<std::string, std::string> {
  std::map<std::string, std::string> *TIEHASH() {
    return $self;
  }
  const char *FIRSTKEY() {
    iterstate[$self] = $self->begin();
    return current(*$self);
  }
  const char *NEXTKEY(const std::string&) {
    ++iterstate[$self];
    return current(*$self);
  }
}

%include <std_map.i>

%typemap(in,noblock=1) const std::map<std::string, std::string>& (void *argp=0, int res=0, $1_ltype tempmap=0) {
  res = SWIG_ConvertPtr($input, &argp, $descriptor, %convertptr_flags);
  if (!SWIG_IsOK(res)) {
    if (SvROK($input) && SvTYPE(SvRV($input)) == SVt_PVHV) {
      fprintf(stderr, "Convert HV to map\n");
      tempmap = new $1_basetype;
      HV *hv = (HV*)SvRV($input);
      HE *hentry;
      hv_iterinit(hv);
      while ((hentry = hv_iternext(hv))) {
        std::string *val=0;
        // TODO: handle errors here
        SWIG_AsPtr_std_string SWIG_PERL_CALL_ARGS_2(HeVAL(hentry), &val);
        fprintf(stderr, "%s => %s\n", HeKEY(hentry), val->c_str());
        (*tempmap)[HeKEY(hentry)] = *val;
        delete val;
      }
      argp = tempmap;
    }
    else {
      %argument_fail(res, "$type", $symname, $argnum); 
    }
  }
  if (!argp) { %argument_nullref("$type", $symname, $argnum); }
  $1 = %reinterpret_cast(argp, $ltype);
}

%typemap(freearg,noblock=1) const std::map<std::string, std::string>& {
  delete tempmap$argnum;
}

%template(StringStringMap) std::map<std::string, std::string>;

%{
#include "stl.h"
%}

%include "stl.h"

I then adapted your sample perl to test:

use Data::Dumper;
use stl;

my $v = stl::TryMap(stl::StringStringMap->new());
$v->{'a'} = '1';
print Dumper $v;
print Dumper stl::TryMap({'a' => '4'});
print Dumper stl::TryMap($v);


foreach my $key (keys %{$v}) {
  print "$key => $v->{$key}\n";
}

print $v->{'7'}."\n";

Which I was able to run successfully:

Got map: 0x22bfb80
$VAR1 = bless( {
                 '7' => '!',
                 'a' => '1'
               }, 'stl::StringStringMap' );
Convert HV to map
a => 4
Got map: 0x22af710
In C++ map: a => 4
$VAR1 = bless( {
                 '7' => '!',
                 'a' => '4'
               }, 'stl::StringStringMap' );
Got map: 0x22bfb20
In C++ map: 7 => !
In C++ map: a => 1
$VAR1 = bless( {
                 '7' => '!',
                 'a' => '1'
               }, 'stl::StringStringMap' );
7 => !
a => 1
!

You can also tie this object to a hash, for example:

use stl;

my $v = stl::TryMap(stl::StringStringMap->new());
print "$v\n";

tie %foo, "stl::StringStringMap", $v;

print $foo{'a'}."\n";
print tied(%foo)."\n";

In theory you can write an out typemap to set up this tie automatically on return from every function call, but so far I've not succeeded in writing a typemap that works with both the tying and the SWIG runtime type system.

It should be noted that this isn't production ready code. There's a thread safety issue for the internal map and some error handling missing too that I know of. I've also not fully tested all of hash operations work from the perl side beyond what you see above. It would also be nice to make it more generic, by interacting with the swig_map_common macro. Finally I'm not a perl guru by any means and I've not used the C API much so some caution in that area would be in order.

Flexo
  • 87,323
  • 22
  • 191
  • 272
  • I'm not even convinced there's a way around [In Perl, how can I return a tied hash from a subroutine?](http://stackoverflow.com/q/4047940) even with the C API. – Flexo Sep 22 '12 at 14:20