Search code examples
c++boost

imbue/facet ignored when used from shared libraries under Windows


I had a wierd behaviour with facet passed to std::ostream through imbue being ignored in a very specific architecture under Android (imbue/facet ignored when used from shared libraries depending on dynamic load order under Android).

While trying to workaround the unresolved issue, I could reproduce the issue under Windows with a much easier architecture. And this is much more surprising to hit this issue in such a simple MCVE:

I declare a shared library linking statically to boost date_time:

bug_datetime_libwin.h:

#pragma once

#ifdef BUG_DATETIME_LIBWIN_EXPORTS
    #define BUG_DATETIME_LIBWIN_API __declspec(dllexport)
#else
    #define BUG_DATETIME_LIBWIN_API __declspec(dllimport)
#endif

#include <ostream>

class BUG_DATETIME_LIBWIN_API BoostFacets
{
public:
    static void SetupStream( std::ostream& str );
    static void PrintTime( std::ostream& str );
};

bug_datetime_libwin.cpp:

#include "bug_datetime_libwin.h"

#ifdef _MSC_VER
#pragma warning(push)
#pragma warning( disable: 4005 ) // if one already included math.h, we get duplicated macro defeinition warnings
#endif
#include <boost/date_time/posix_time/posix_time.hpp>
#ifdef _MSC_VER
#pragma warning(pop)
#endif

#define READABLE_DATE_FORMAT "%Y-%b-%d"
#define READABLE_TIME_FORMAT_ACC "%H:%M:%s"

void BoostFacets::SetupStream( std::ostream& str )
{
    static std::string accurateFormat = std::string(READABLE_DATE_FORMAT) + " " + READABLE_TIME_FORMAT_ACC;
    
    boost::posix_time::time_facet* facet = new boost::posix_time::time_facet(accurateFormat.c_str());
    str.imbue(std::locale(str.getloc(), facet));
}

void BoostFacets::PrintTime( std::ostream& str )
{
    SetupStream( str );
    boost::posix_time::ptime t1(boost::gregorian::date(2002,boost::gregorian::Jan,10),
                                boost::posix_time::time_duration(1,2,4));
    str << t1;
}

Then I have an application linking dynamically to the shared library and statically to boost date_time:

main.cpp:

#include <sstream>
#include <ostream>
#include <boost/date_time/posix_time/posix_time.hpp>

#include "bug_datetime_libwin/bug_datetime_libwin.h"

int main( int argc, char* argv[] )
{
    {
        std::stringstream temp;
        BoostFacets::PrintTime( temp );

        std::cout << "PrintTime: " << temp.str().c_str() << std::endl;
    }

    {
        std::stringstream temp;
        BoostFacets::SetupStream( temp );

        boost::posix_time::ptime t1(boost::gregorian::date(2002,boost::gregorian::Jan,10),
                                    boost::posix_time::time_duration(1,2,4));
        temp << t1;

        std::cout << "SetupStream: " << temp.str().c_str() << std::endl;
    }

    return 0;
}

This function outputs:

PrintTime: 2002-Jan-10 01:02:04.000000
SetupStream: 2002-Jan-10 01:02:04

As you can see, if the lib does an imbue on a stream, creates a boost::posix_time::ptime object and prints it to the stream, it works fine.

But if lib does an imbue on a stream, and the application creates a boost::posix_time::ptime object and prints it to the string, it does not work fine: imbue has no effect!

What am I doing wrong?


Update 1:

Tried to compile boost as shared libraries to prevent ODR violation as proposed by Alan Birtles. It did not fix the issue. Actually, boost does not even need to be linked for this code to work as used mincludes are "header-only". So the issue is not due to static boost linkage.


Update 2:

I used the debugger to see what's going on:

The issue comes from:

template <class CharT, class TraitsT>
  inline
  std::basic_ostream<CharT, TraitsT>&
  operator<<(std::basic_ostream<CharT, TraitsT>& os,
             const ptime& p) {
    boost::io::ios_flags_saver iflags(os);
    typedef boost::date_time::time_facet<ptime, CharT> custom_ptime_facet;
    std::ostreambuf_iterator<CharT> oitr(os);
    if (std::has_facet<custom_ptime_facet>(os.getloc()))
      std::use_facet<custom_ptime_facet>(os.getloc()).put(oitr, os, os.fill(), p);
    else {
      //instantiate a custom facet for dealing with times since the user
      //has not put one in the stream so far.  This is for efficiency 
      //since we would always need to reconstruct for every time period
      //if the locale did not already exist.  Of course this will be overridden
      //if the user imbues as some later point.
      custom_ptime_facet* f = new custom_ptime_facet();
      std::locale l = std::locale(os.getloc(), f);
      os.imbue(l);
      f->put(oitr, os, os.fill(), p);
    }
    return os;
  }

in posix_time_io.hpp.

  • When main calls PrintTime, it calls SetupStream, here created facet id (boost::posix_time::time_facet::id) is 5
  • Then when SetupStream calls str << t1, custom_ptime_facet, in posix_time_io.hpp's operator<<, has an id value of 5, so has_facet resturns true and string gets formatted.
  • When main calls SetupStream, here created facet id (boost::posix_time::time_facet::id) is still 5
  • But when main calls str << t1, custom_ptime_facet, in posix_time_io.hpp's operator<<, has an id value of 44, so has_facet resturns false and string does not get formatted.

I suspect this is because posix_time_io.hpp is included twice:

  • once from the library, assigning static custom_ptime_facet::id to 5
  • once from the program, assigning static custom_ptime_facet::id to 44

So I tested, and doing this at the end of the MCVE's main function:

{
    static std::string accurateFormat = std::string(READABLE_DATE_FORMAT) + " " + READABLE_TIME_FORMAT_ACC;

    std::stringstream temp;
    boost::posix_time::time_facet* facet = new boost::posix_time::time_facet(accurateFormat.c_str());
    temp.imbue(std::locale(temp.getloc(), facet));

    boost::posix_time::ptime t1(boost::gregorian::date(2002,boost::gregorian::Jan,10),
                                boost::posix_time::time_duration(1,2,4));
    temp << t1;

    std::cout << "main: " << temp.str().c_str() << std::endl;
}

outputs PrintTime: 2002-Jan-10 01:02:04.000000. It works because facet created here has an id of 44, then has_facet returns true.

Does that mean that, by construction, because it uses header-only approach, boost date time won't allow one module to set imbue and another to use operator<<?


Solution

  • You have an ODR violation. As boost::posix_time::time_facet is a header only template, each translation unit gets its own copy of the template instantiations. The linker merges any duplicates into a single object when creating a shared library or executable. However the existence of the object inside the shared library is not visible to the linker when creating the executable so the executable gets its own copy of the object. This then means there are two separate instances of the static std::locale::id id member which the standard library (correctly) assigns different values.

    To fix the issue you have to both export time_facet from your shared library and make sure that your executable uses the shared library version rather than creating its own copy.

    In your shared library in one translation unit add:

    template class __declspec(dllexport)
    boost::date_time::time_facet<boost::posix_time::ptime, char>;
    

    In your application after including boost but before any instantiations of the facet add:

    template class __declspec(dllimport)
    boost::date_time::time_facet<boost::posix_time::ptime, char>;
    

    There are many pitfalls in interoperating with c++ objects over shared library boundaries. Its much easier to statically link everything into a single executable.