从std::fstream获取FILE*

80

有没有一种(跨平台的)方法可以从C++ std :: fstream获取C FILE*句柄?

我问这个问题是因为我的C ++库接受fstream,而在一个特定的函数中,我想使用一个接受FILE *的C库。

9个回答

50

简短的回答是不行。

原因是,std::fstream不需要使用FILE*作为其实现的一部分。因此,即使您设法从std::fstream对象中提取文件描述符并手动构建一个FILE对象,那么您现在将有两个缓冲对象写入相同的文件描述符,会导致其他问题。

真正的问题是,为什么要将std::fstream对象转换为FILE*

虽然我不推荐这样做,但你可以尝试查找funopen()
不幸的是,这不是POSIX API(它是BSD扩展),因此它的可移植性存在问题。这也可能是为什么我找不到任何人用类似于这样的对象包装了std::stream

FILE *funopen(
              const void *cookie,
              int    (*readfn )(void *, char *, int),
              int    (*writefn)(void *, const char *, int),
              fpos_t (*seekfn) (void *, fpos_t, int),
              int    (*closefn)(void *)
             );

这样可以让你构建一个FILE对象并指定一些将用于执行实际工作的函数。如果编写适当的函数,您可以让它们从实际打开文件的std::fstream对象中读取。


6
很遗憾它只适用于BSD;如果能让任何类型的C++流使用FILE*,那它本来会是一个很好的解决方案。 - Jay Mooney
3
你问了“为什么?”:因为一个人可能已经有了用C语言编写的打印功能的C实现,想要在C++输出流(或输出文件流)中重用它。 - alfC
1
为什么?因为重载operator<<操作符在使用对象时非常方便,但格式化流输出可能会很混乱和痛苦。使用fprintf()进行格式化紧凑且易于操作。换句话说,我想能够编写out << someObject << anotherObject,但使用fprintf(ofp, "%8.1lf %2d\n", doubleVar, intVar)来实现operator<<操作符。 - riderBill
1
如果你想/需要使用“printf”格式化范例,你可以随时使用sprintf创建一个C字符串,然后将其发送到你的ofstream。当然,这意味着你需要对格式化字符串长度的上限有所了解,以便正确地调整char[]的大小。 - MikeMayer67
2
GNU的fopencookie函数具有相同的功能,但接口略有不同。 - Chris Dodd
显示剩余2条评论

19

目前还没有标准化的方式。我猜这是因为C++标准化组不想假设文件句柄可以表示为fd。

大多数平台似乎提供了一些非标准化的方法来实现此操作。

http://www.ginac.de/~kreckel/fileno/ 提供了一个很好的阐述情况的文档,并提供了代码,可以隐藏所有平台特定的细节,至少对于GCC而言。考虑到在GCC上实现这个过程有多么困难,如果可能的话,我会完全避免使用这种方法。


4
FILE*和文件描述符是不同的对象,它们被不同的组件使用。其中一个被C运行时库使用,另一个被操作系统使用。请参见文件描述符和文件指针有什么区别? - jww

13

更新:请参阅@Jettatura,我认为这是最好的答案https://dev59.com/oHVD5IYBdhLWcg3wDG_m#33612982(仅适用于Linux?)。

原始:

(可能不跨平台,但简单)

简化http://www.ginac.de/~kreckel/fileno/(dvorak答案)中的hack,并查看此gcc扩展http://gcc.gnu.org/onlinedocs/gcc-4.6.2/libstdc++/api/a00069.html#a59f78806603c619eafcd4537c920f859, 我有一个解决方案,在C++11之前可以在GCC(至少是4.8)和clang(至少是3.3)上使用:

#include<fstream>
#include<ext/stdio_filebuf.h>

typedef std::basic_ofstream<char>::__filebuf_type buffer_t;
typedef __gnu_cxx::stdio_filebuf<char>            io_buffer_t; 
FILE* cfile_impl(buffer_t* const fb){
    return (static_cast<io_buffer_t* const>(fb))->file(); //type std::__c_file
}

FILE* cfile(std::ofstream const& ofs){return cfile_impl(ofs.rdbuf());}
FILE* cfile(std::ifstream const& ifs){return cfile_impl(ifs.rdbuf());}

并且可以这样使用:

int main(){
    std::ofstream ofs("file.txt");
    fprintf(cfile(ofs), "sample1");
    fflush(cfile(ofs)); // ofs << std::flush; doesn't help 
    ofs << "sample2\n";
}

注意:在库的更新版本中不再使用`stdio_filebuf`。`static_cast<>()`也有一定风险,如果你得到一个不是正确类别的`nullptr`,可以使用`dynamic_cast<>()`代替。你也可以尝试使用`stdio_sync_filebuf`。但是这个类的问题是`file()`不再可用。
限制(欢迎评论):
1.我发现在`fprintf`打印到`std::ofstream`后需要使用`fflush`,否则在上面的例子中,“sample2”会出现在“sample1”之前。我不知道是否有比使用`fflush`更好的解决方法。特别是`ofs << flush`没有帮助。 2.无法从`std::stringstream`中提取`FILE*`,我甚至不知道是否可能。(下面的内容中有更新说明)
3.我仍然不知道如何从`std::cerr`等中提取C语言中的`stderr`,例如在类似这样的假设代码中使用 `fprintf(stderr, "sample")`,如`fprintf(cfile(std::cerr), "sample")`。
关于最后一个限制,我找到的唯一解决方法是添加这些重载。
FILE* cfile(std::ostream const& os){
    if(std::ofstream const* ofsP = dynamic_cast<std::ofstream const*>(&os)) return cfile(*ofsP);
    if(&os == &std::cerr) return stderr;
    if(&os == &std::cout) return stdout;
    if(&os == &std::clog) return stderr;
    if(dynamic_cast<std::ostringstream const*>(&os) != 0){
       throw std::runtime_error("don't know cannot extract FILE pointer from std::ostringstream");
    }
    return 0; // stream not recognized
}
FILE* cfile(std::istream const& is){
    if(std::ifstream const* ifsP = dynamic_cast<std::ifstream const*>(&is)) return cfile(*ifsP);
    if(&is == &std::cin) return stdin;
    if(dynamic_cast<std::ostringstream const*>(&is) != 0){
        throw std::runtime_error("don't know how to extract FILE pointer from std::istringstream");
    }
    return 0; // stream not recognized
}

尝试处理 iostringstream

通过使用 fmemopen 可以从 istream 中使用 fscanf 进行读取,但如果想要结合 C 语言的读取和 C++ 语言的读取,则需要进行大量的记录和更新流的输入位置。我无法将其转换为像上面的 cfile 函数那样的函数。(也许一个每次读取后都会更新的 cfile 类是正确的方法)。请保留 HTML 标签。

// hack to access the protected member of istreambuf that know the current position
char* access_gptr(std::basic_streambuf<char, std::char_traits<char>>& bs){
    struct access_class : std::basic_streambuf<char, std::char_traits<char>>{
        char* access_gptr() const{return this->gptr();}
    };
    return ((access_class*)(&bs))->access_gptr();
}

int main(){
    std::istringstream iss("11 22 33");
    // read the C++ way
    int j1; iss >> j1;
    std::cout << j1 << std::endl;

    // read the C way
    float j2;
   
    char* buf = access_gptr(*iss.rdbuf()); // get current position
    size_t buf_size = iss.rdbuf()->in_avail(); // get remaining characters
    FILE* file = fmemopen(buf, buf_size, "r"); // open buffer memory as FILE*
    fscanf(file, "%f", &j2); // finally!
    iss.rdbuf()->pubseekoff(ftell(file), iss.cur, iss.in); // update input stream position from current FILE position.

    std::cout << "j2 = " << j2 << std::endl;

    // read again the C++ way
    int j3; iss >> j3;
    std::cout << "j3 = " << j3 << std::endl;
}

太好了!最近我需要对一个以 ofstream 形式输入的内容进行 tcsetattr() 处理,你的文章真的帮了我很大的忙。 - Paul Mikesell
3
同意。阅读Boost.Interprocess的文档后,发现有一些特定于平台的行为会使标准化变得麻烦。尽管如此,缺少这个库还是存在一个很大的空缺,所以我认为值得努力尝试让平台开发人员支持一个通用的行为子集,为将来C++标准的支持奠定基础。 - Droid Coder
1
这或许应该成为被采纳的答案。我在我的问题 https://dev59.com/oHVD5IYBdhLWcg3wDG_m 中引用了它,这个问题是由使用 std::freadstd::ifstream::read 带来的性能提升所激发的。 - Oliver Schönrock
1
@OliverSchönrock,这个问题是由一个未注册的用户(Bek?)在2008年提出的,我怀疑他是否还在或能够恢复帐户以接受答案。我不知道管理员是否可以接受答案。 - alfC
1
我没有。对于我的情况来说,代码有点太多了,而且仅适用于Linux,我更喜欢你的代码。特别是因为我也需要查找,看起来在那个实现中缺少了?顺便说一句,我上面粘贴的链接是错误的。这是我的问题:https://stackoverflow.com/questions/69435310/how-to-get-file-from-a-stdifstream-in-order-to-call-stdfread - Oliver Schönrock
显示剩余6条评论

5

好的,你可以获取文件描述符 - 我忘记方法是fd()还是getfd()。我使用的实现提供了这样的方法,但是我相信语言标准不要求它们 - 标准不应该关心你的平台是否使用fd来处理文件。

从那里,您可以使用fdopen(fd, mode)获取FILE*。

但是,我认为标准所需的用于同步STDIN / cin,STDOUT / cout和STDERR / cerr的机制不必对您可见。因此,如果您同时使用fstream和FILE*,缓冲可能会使您混乱。

另外,如果fstream或FILE中任何一个关闭,它们可能会关闭底层fd,因此在关闭任何一个之前,您需要确保将两者都刷新。


1
抱歉,这将取决于您使用的编译器/库。对于libstdc++,它是fd(),看起来像这样:https://gcc.gnu.org/onlinedocs/libstdc++/manual/ext_io.html - Mike G.

4
在Linux中另一种做法是:
#include <stdio.h>
#include <cassert>

template<class STREAM>
struct STDIOAdapter
{
    static FILE* yield(STREAM* stream)
    {
        assert(stream != NULL);

        static cookie_io_functions_t Cookies =
        {
            .read  = NULL,
            .write = cookieWrite,
            .seek  = NULL,
            .close = cookieClose
        };

        return fopencookie(stream, "w", Cookies);
    }

    ssize_t static cookieWrite(void* cookie,
        const char* buf,
        size_t size)
    {
        if(cookie == NULL)
            return -1;

        STREAM* writer = static_cast <STREAM*>(cookie);

        writer->write(buf, size);

        return size;
    }

    int static cookieClose(void* cookie)
    {
         return EOF;
    }
}; // STDIOAdapter

例如使用方法:
#include <boost/iostreams/filtering_stream.hpp>
#include <boost/iostreams/filter/bzip2.hpp>
#include <boost/iostreams/device/file.hpp>

using namespace boost::iostreams;

int main()
{   
    filtering_ostream out;
    out.push(boost::iostreams::bzip2_compressor());
    out.push(file_sink("my_file.txt"));

    FILE* fp = STDIOAdapter<filtering_ostream>::yield(&out);
    assert(fp > 0);

    fputs("Was up, Man", fp);

    fflush (fp);

    fclose(fp);

    return 1;
}

2
非常好,我会用template<class Stream> FILE* cfile(Stream& s){ return STDIOAdapter<Stream>::yield(&s); }来补充它。它可以与std::coutstd::cerr以及混合使用fprintf和C++代码。我建议使用这个。 - alfC
1
我再次评论以确认这适用于std::ostringstream - alfC
有没有必要也加入seek函数呢?int cookieSeek(void* cookie, ssize_t* off, int way){ static_cast<std::iostream*>(cookie)->seekg(*off, static_cast<Stream*>(cookie)->end); return way; }? (我不确定我是否正确使用了way参数。) - alfC
我不明白这里发生了什么。这个关于cookies的事情是什么意思? - einpoklum
@Jettatura:所以,这是非标准的。 - einpoklum

3
在单线程POSIX应用程序中,您可以轻松地以可移植的方式获取fd编号:
int fd = dup(0);
close(fd);
// POSIX requires the next opened file descriptor to be fd.
std::fstream file(...);
// now fd has been opened again and is owned by file

如果此代码与其他线程打开文件描述符的操作同时进行,该方法在多线程应用程序中会出现问题。


2

有一种方法可以从 fstream 中获取文件描述符,然后通过 fdopen 将其转换为 FILE*。个人认为没有必要使用 FILE*,但是通过文件描述符,您可以进行许多有趣的操作,比如重定向 (dup2)。

解决方案:

#define private public
#define protected public
#include <fstream>
#undef private
#undef protected

std::ifstream file("some file");
auto fno = file._M_filebuf._M_file.fd();

最后一个字符串适用于libstdc ++。如果您正在使用其他库,则需要进行一些反向工程。这个技巧有点不道德,并且会暴露所有fstream的私有和公共成员。如果您想在生产代码中使用它,我建议您创建单独的.cpp和.h文件,其中包含一个名为int getFdFromFstream(std::basic_ios<char>& fstr);的函数。头文件不得包含fstream。

什么?这不仅很糟糕,而且存在未定义的行为。绝对不能使用。 - JHBonarius
@JHBonarius,如果是UB,那么什么都不会起作用。即使你不使用#define技巧。C++始终创建完全相同的结构,无论一切是否公开。 - Alexis Wilke
1
@AlexisWilke 嗯,那通常并不正确。UB只是意味着“行为未被标准定义”。在实践中,“可能”有效。但是,没有任何保证。它可能会从构建到构建甚至从运行到运行中断。因此,您不应依赖UB。但是,也有一些UB的行为由编译器定义,并且已经使用了很多年,例如因为没有其他方法。但这不是好的做法,而且通常会将自己锁定在特定的平台和编译器版本中。尽可能避免UB。 - JHBonarius
在这种情况下,_M_filebuf 已经是一个 libstdc++ 特定的内部实现。由于它没有在 C++ 标准中定义,因此他们可以自由地在版本之间进行修改或删除。您没有任何保证,并且链接到另一个库版本可能会破坏您的代码,或者更糟糕的是,运行不同的二进制文件可能会导致糟糕的结果发生。 - JHBonarius
实际上,stdio_filebuf(C++98)和stdio_sync_filebuf(C++11及更高版本)已经有两个实现。所以我们已经有了这样的东西。旧的实现方式可以让你访问file()。上面的代码在C++98版本中无法编译。关于不兼容的二进制文件,我偶尔也会遇到这个问题。我经常从头开始重新编译整个项目(到目前为止有902个C++文件),因为它会崩溃...在大多数情况下,这是因为我改变了一个类,因此字段的布局在版本之间发生了变化,而我错过了编译/安装修改后的库。 - Alexis Wilke

1

当我面临只能在文件描述符上运行的isatty()时,我遇到了这个问题。

在新版本的C++标准库中(至少从C++11开始),alfC提出的解决方案不再适用,因为那个类已经改变成了一个新类。

旧方法仍然可以在使用非常旧的编译器版本时使用。在更新的版本中,您需要使用std::basic_filebuf<>()。但是,这对于标准I/O(如std::cout)无效。对于这些内容,您需要使用__gnu_cxx::stdio_sync_filebuf<>()

我在我的isatty() C++流实现中有一个功能示例。您应该能够将那个文件拿走并在自己的项目中重复使用它。不过,在您的情况下,您想要FILE*指针,所以只需返回它而不是::isatty(fileno(<of FILE*>))的结果即可。

以下是模板函数的副本:

template<typename _CharT
       , typename _Traits = std::char_traits<_CharT>>
bool isatty(std::basic_ios<_CharT, _Traits> const & s)
{
    { // cin, cout, cerr, and clog
        typedef __gnu_cxx::stdio_sync_filebuf<_CharT, _Traits> io_sync_buffer_t;
        io_sync_buffer_t * buffer(dynamic_cast<io_sync_buffer_t *>(s.rdbuf()));
        if(buffer != nullptr)
        {
            return ::isatty(fileno(buffer->file()));
        }
    }

    { // modern versions
        typedef std::basic_filebuf<_CharT, _Traits> file_buffer_t;
        file_buffer_t * file_buffer(dynamic_cast<file_buffer_t *>(s.rdbuf()));
        if(file_buffer != nullptr)
        {
            typedef detail::our_basic_filebuf<_CharT, _Traits> hack_buffer_t;
            hack_buffer_t * buffer(static_cast<hack_buffer_t *>(file_buffer));
            if(buffer != nullptr)
            {
                return ::isatty(fileno(buffer->file()));
            }
        }
    }

    { // older versions
        typedef __gnu_cxx::stdio_filebuf<_CharT, _Traits> io_buffer_t;
        io_buffer_t * buffer(dynamic_cast<io_buffer_t *>(s.rdbuf()));
        if(buffer != nullptr)
        {
            return ::isatty(fileno(buffer->file()));
        }
    }

    return false;
}

现在,你可能会问: 但是那个细节类our_basic_filebuf是什么???
这是一个很好的问题。事实上,_M_file指针是受保护的,并且在std::basic_filebuf中没有file()(或fd())。因此,我创建了一个外壳类,它可以访问受保护的字段,这样我就可以返回FILE*指针。
template<typename _CharT
       , typename _Traits = std::char_traits<_CharT>>
class our_basic_filebuf
    : public std::basic_filebuf<_CharT, _Traits>
{
public:
    std::__c_file * file() throw()
    {
        return this->_M_file.file();
    }
};

这样做可能有点丑陋,但是我能想到的最干净的方法来访问_M_file字段。


0

Alexis Wilke的想法修订 它在我的MSVC 2022和MSYS2下运行良好

namespace detail {

#ifdef _WIN32
using handle_type = HANDLE;
#else
#define INVALID_HANDLE_VALUE (-1)
using handle_type = int;
#endif

template <
    typename _Elem,
    typename _Traits = std::char_traits<_Elem>
>
class basic_filebuf_hack :
#ifdef _MSC_VER
public std::basic_streambuf<_Elem, _Traits> {
public:
    using _Cvt  = std::codecvt<_Elem, char, typename _Traits::state_type>;

    const _Cvt * _Pcvt;
    _Elem        _Mychar;
    bool         _Wrotesome;
    typename _Traits::state_type _State;
    bool         _Closef;
    FILE       * _Myfile;
    _Elem      * _Set_eback;
    _Elem      * _Set_egptr;
    
    __forceinline
    auto file() throw() {
        return this->_Myfile;
    }
#else
public std::basic_filebuf<_Elem, _Traits> {
    auto file() throw() {
        return this->_M_file.file();
    }
#endif
};
static_assert(sizeof(std::basic_filebuf<char>) == sizeof(basic_filebuf_hack<char>), "sizes not same");

} // namespace detail

template <
    typename _CharT,
    typename _Traits = std::char_traits<_CharT>
>
detail::handle_type ios_fileno(const std::basic_ios<_CharT, _Traits> & s) {
    using file_buffer_t = std::basic_filebuf<_CharT, _Traits>;
    auto file_buffer = dynamic_cast<file_buffer_t *>(s.rdbuf());

    if (file_buffer != nullptr) {
        using hack_buffer_t = detail::basic_filebuf_hack<_CharT, _Traits>;
        auto buffer = reinterpret_cast<hack_buffer_t *>(file_buffer);

        if (buffer != nullptr) {
            auto file = buffer->file();

            if (file != nullptr) {
#ifdef _WIN32
                return detail::handle_type(_get_osfhandle(_fileno(file)));
#else
                return fileno(file);
#endif
            }
        }
    }

    return INVALID_HANDLE_VALUE;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接