2

I have a Qt project which have a set of source/header files that are also used in other projects that are not Qt based. These files handle reading of .csv files (I'll call it my CSVReader class). The CSVReader is written without any Qt specific function calls (I have control over modification of the CSVReader, but with the requirement that it doesn't use any Qt specific code).

In my Qt based application I like these extra .csv files to be embedded into the .exe using the .qrc file this way a user cannot accidentally delete or modify these files.

Obviously this poses a problem when reading this data in since my CSVReader uses calls like fopen and fread

I was hoping I could use something like the following (Convert QFile to FILE*) in the Qt part of my project and just pass the file handle to the CSVReader.

QFile myFile("goforward.raw");
myFile.open(QIODevice::ReadOnly);
int fileHandle = myFile.handle();
FILE* fh = fdopen(fileHandle, "rb");

But obviously since the file only exists in the .exe the call to myFile.handle() returns -1.

My current idea (which is kind of ugly) is to open the file using QFile, then write the file to the harddrive, then load the file in the CSVReader using FILE *f = fopen(fname, "rt");, and after that I delete the file that I wrote.

If any others have ideas on how to read or open the qresource file I'm open to other ideas.

Thanks

Community
  • 1
  • 1
Stanton
  • 904
  • 10
  • 25
  • From a short Google search you might be able to read the file into memory (e.g. into a QByteArray) and then use `fmemopen` to get a `FILE*`. I have absolutely no idea if this really works and has any problems/side effects, but maybe it's worth a try? – Simon Kraemer Nov 30 '16 at 18:29
  • Some links to this approach: http://stackoverflow.com/questions/539537/memory-buffer-as-file http://stackoverflow.com/questions/12249610/c-create-file-in-memory http://stackoverflow.com/a/5860435/4181011 – Simon Kraemer Nov 30 '16 at 18:33

3 Answers3

2

The CSVReader can do either of two things:

  1. Parse from memory - the file would have to be memory-mapped first. This makes sense on 64 bit platforms where you won't run out of virtual memory. But on 32 bit platforms this is not very flexible: you won't be able to open any file over a gigabyte or two in size. The CSVReader would work off a const char *, size_t pair.

    Note that memory mapping is not the same as explicitly reading from a file. When you memory-map a file, the reading is done by the operating system on your behalf: you never do any reading directly from the file yourself.

    If the files are small enough to fit into virtual memory on 32 bit platforms, or if you're on a 64-bit platform, this will be most likely the best performing approach as the page mapping system of modern kernels will offer least impedance mismatch between the IO device and the parser.

  2. Read the data incrementally from a file using an abstract interface. The CSVReader would work off an InputInterface instance. The reader should expect an open instance of the interface.

    The reader should not open the file itself, as opening is specific to the particular implementation. Since a QFile-based implementation will accept resource paths while a standard library-based one won't, it makes no sense having a generic open method: it will hide errors that otherwise wouldn't be possible by construction.

The second approach seems to have widest applicability. You could define the interface as follows:

// https://github.com/KubaO/stackoverflown/tree/master/questions/file-interface-40895489
#include <cstdint>

class InputInterface {
protected:
   InputInterface() {}
public:
   virtual int64_t read(char *, int64_t maxSize) = 0;
   virtual int64_t pos() const = 0;
   virtual bool seek(int64_t) = 0;
   virtual bool isOpen() const = 0;
   virtual bool atEnd() const = 0;
   virtual bool ok() const = 0;
   virtual bool flush() = 0;
   virtual void close() = 0;
   virtual ~InputInterface() {}
};

The QFile-based implementation could look as follows:

#include <QtCore>

class QtFile : public InputInterface {
   QFile f;
public:
   QtFile() {}
   QtFile(const QString &name) : f(name) {}
   bool open(const QString &name, QFile::OpenMode mode) {
      close();
      f.setFileName(name);
      return f.open(mode);
   }
   bool open(QFile::OpenMode mode) {
      close();
      return f.open(mode);
   }
   void close() override {
      f.close();
   }
   bool flush() override {
      return f.flush();
   }
   int64_t read(char * buf, int64_t maxSize) override {
      return f.read(buf, maxSize);
   }
   int64_t pos() const override {
      return f.pos();
   }
   bool seek(int64_t pos) override {
      return f.seek(pos);
   }
   bool isOpen() const override {
      return f.isOpen();
   }
   bool atEnd() const override {
      return f.atEnd();
   }
   bool ok() const override {
      return f.isOpen() && f.error() == QFile::NoError;
   }
   QString statusString() const {
      return f.errorString();
   }
};

The plain C++ implementation would be:

#include <cstdio>
#include <cerrno>
#include <cassert>
#include <string>

class CFile : public InputInterface {
   FILE *f = nullptr;
   int mutable m_status = 0;
public:
   CFile() {}
   CFile(FILE * f) : f(f) {
      assert(!ferror(f)); // it is impossible to retrieve the error at this point
      m_status = 0;
   }
   ~CFile() { close(); }
   void close() override {
      if (f) fclose(f);
      f = nullptr;
      m_status = 0;
   }
   bool open(const char *name, const char *mode) {
      close();
      f = fopen(name, mode);
      if (!f) m_status = errno;
      return f;
   }
   bool flush() override {
      auto rc = fflush(f);
      if (rc) m_status = errno;
      return !rc;
   }
   bool isOpen() const override { return f; }
   bool atEnd() const override { return f && feof(f); }
   bool ok() const override { return f && !m_status; }
   int64_t read(char * buf, int64_t maxSize) override {
      auto n = fread(buf, 1, maxSize, f);
      if (ferror(f)) m_status = errno;
      return n;
   }
   bool seek(int64_t pos) override {
      auto rc = fseek(f, pos, SEEK_SET);
      if (rc) m_status = errno;
      return !rc;
   }
   int64_t pos() const override {
      if (!f) return 0;
      auto p = ftell(f);
      if (p == EOF) {
         m_status = errno;
         return 0;
      }
      return p;
   }
   std::string statusString() const {
      return {strerror(m_status)};
   }
};
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Thank you for the very detailed approach. All of my code will be destined for 64 bit machines so memory is not a problem. For that reason, I intend to use the first of your two approaches, however the InputInterface idea is very interesting so I will definitely keep it in mind if I have the need for it later. – Stanton Nov 30 '16 at 20:49
0

I chose to take @Kuba's first approach since I don't have memory limitations in my application. For those interested I've posted my plain C++ and Qt-based approaches below.

Plain C++

std::ifstream ifs("file.csv");
std::string fileContents((std::istreambuf_iterator<char>(ifs)),
                         (std::istreambuf_iterator<char>()));
CSVReader csvReader(fileContents);

Qt-based

QFile qF(":/file.csv");
if (qF.open(QFile::ReadOnly))
{
    QTextStream qTS(&qF);
    CSVReader csvReader(qTS.readAll().toStdString());
}
Stanton
  • 904
  • 10
  • 25
  • 1
    No, `readAll()` will have to actually **allocate memory for all data in the file you are reading** (since it is returning a `QByteArray` with all file contents). @KubaOber's approach was about **mapping file data to virtual memory**, this would be done in Qt using something like [`QFileDevice::map()`](https://doc.qt.io/qt-5/qfiledevice.html#map). So, you use that in your application, and pass the result pointer to your C++-only `CSVReader`. – Mike Nov 30 '16 at 23:50
  • I agree with Mike. You don't want any of what you show above. Use `QFile::map`, or a platform-specific file memory-mapped API, as needed. The `csvReader` should probably treat the file as if it was encoded using an ASCII-compatible encoding, thus UTF-8, Latin1, etc. Transcoding on reading of CSVs will unnecessarily rob performance. – Kuba hasn't forgotten Monica Dec 01 '16 at 04:31
  • So for the Qt implementation I would use `const char *data = qF.map(0,qF.size());` and give `data` and `qF.size()` as inputs to `CSVReader`. What would the non-Qt implementation look like? I would expect for the inputs to the `CSVReader` to be exactly the same (i.e. `const char*` and `size_t`) – Stanton Dec 01 '16 at 20:23
0

You could take advantage of Qt QTemporaryFile to copy your data and start. QTemporaryfile works on every os.

Here is an example (this temporary file is associated to the entire qApp so that it will be removed once you quit the application):

QTemporaryFile tmpFile(qApp);
tmpFile.setFileTemplate("XXXXXX.csv");
if (tmpFile.open()) {
    QString tmp_filename=tmpFile.fileName();
    qDebug() << "temporary" << tmp_filename;

    QFile file(":/file.csv");
    if (file.open(QIODevice::ReadOnly)) {
        tmpFile.write(file.readAll());
    }

    tmpFile.close();
}

The you can reopen the file tmpFile.fileName()

bibi
  • 3,671
  • 5
  • 34
  • 50