9
\$\begingroup\$

I have this great C program I'd like to embed into an iOS app. One passes command line arguments to it and the results are printed to stdout via printf and fputs - like with all the good old Unix programs.

Now I'd like to just edit main and the print functions to use my own printf function which collects all the output that normally goes to stdout and return it at the end.

I implemented a solution by using a line buffer to collect all the printfs until the newline. And a dynamic char array whereto I copy when an output line is finished.

The charm of this solution is - it's kind of tcl'ish: just throw everything into a text line, and if it's complete, store it. Now do that as long as necessary and return the whole bunch at the end.

C code:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

// outLineBuffer collects one output line by several calls to tprntf
#define initialSizeOfReturnBuffer 10 // reduced for testing (would be 16*1024)
#define incrSizeOfReturnBuffer 5     // reduced for testing (would be 1024*1024)
#define outLineBufferMaxSize 4095
char outLineBuffer[sizeof(char)*outLineBufferMaxSize] = "";
char *tReturnString;
size_t sizeOfReturnBuffer, curPosOutBuffer = 0, lenOutLine = 0;

With the replacement tprntf for all the original printf and fputs:

// replace printf with this to collect the parts of one output line.
static int tprntf(const char *format, ...)
{
    const size_t maxLen = sizeof(char)*outLineBufferMaxSize;
    va_list arg;
    int done;

    va_start (arg, format);
    done = vsnprintf (&outLineBuffer[lenOutLine], maxLen-lenOutLine, format, arg);
    va_end (arg);
    lenOutLine = strlen(outLineBuffer);

    return done;
}

And the function when we complete one output line (everywhere \n is printed):

// Output line is now complete: copy to return buffer and reset line buffer.
static void tprntNewLine()
{
    size_t newSize;
    long remainingLenOutBuffer;
    char *newOutBuffer;

    remainingLenOutBuffer = sizeOfReturnBuffer-curPosOutBuffer-1;
    lenOutLine = strlen(outLineBuffer)+1; // + newline character (\n)
    remainingLenOutBuffer -= lenOutLine;

    if (remainingLenOutBuffer < 0) {
        newSize = sizeOfReturnBuffer + sizeof(char)*incrSizeOfReturnBuffer;
        if ((newOutBuffer = realloc(tReturnString, newSize)) != 0) {
            tReturnString = newOutBuffer;
            sizeOfReturnBuffer = newSize;
        } else {
            lenOutLine += remainingLenOutBuffer; //just write part that is still available
            remainingLenOutBuffer = 0;
        }
    }

    snprintf(&tReturnString[curPosOutBuffer], lenOutLine+1, "%s\n", outLineBuffer);

    curPosOutBuffer += lenOutLine;
    outLineBuffer[0] = 0;
    lenOutLine = 0;
}

And a little main to test it (without Swift - e.g. plain gcc):

int main(int argc, char *argv[])
{
    int i;
    sizeOfReturnBuffer = initialSizeOfReturnBuffer*sizeof(char);
    if ((tReturnString = malloc(sizeOfReturnBuffer)) == 0) {
        return 1; // "Sorry we are out of memory. Please close other apps and try again!";
    }
    tReturnString[0] = 0;

    for (i = 1; i < argc; i++) {
        tprntf("%s ", argv[i]);
    }
    tprntNewLine();

    tprntf("%s", "ABC\t");
    tprntf("%d", 12);
    tprntNewLine(); // enough space for that ;-)
    tprntf("%s", "DEF\t");
    tprntf("%d", 34);
    tprntNewLine(); // realloc necessary ...
    tprntf("%s", "GHI\t");
    tprntf("%d", 56);
    tprntNewLine(); // again realloc for testing purposes ...
    printf("tReturnString at the end:\n>%s<\n", tReturnString); // contains trailing newline
    return 0;
}

The call from swift will then be as follows (using CStringArray.swift):

let myArgs = CStringArray(["computeIt", "par1", "par2"])
let returnString = mymain(myArgs.numberOfElements, &myArgs.pointers[0])

if let itReturns = String.fromCString(returnString) {
    print(itReturns)
}

freeMemory()
\$\endgroup\$
0

2 Answers 2

5
\$\begingroup\$

Bug

Your code in tprntNewLine() doesn't guarantee that tReturnString is large enough to hold the output string.

Suppose at entry to the function, these are the values of your variables:

sizeOfReturnBuffer = 10;
curPosOutBuffer    = 0;

And then you get an outLine whose length is 80. After this piece of code:

if (remainingLenOutBuffer < 0) {
    newSize = sizeOfReturnBuffer + sizeof(char)*incrSizeOfReturnBuffer;
    if ((newOutBuffer = realloc(tReturnString, newSize)) != 0) {
        tReturnString = newOutBuffer;
        sizeOfReturnBuffer = newSize;
    } else {
        lenOutLine += remainingLenOutBuffer; //just write part that is still available
        remainingLenOutBuffer = 0;
    }
}

You will increase the size of your output buffer from 10 to 15 (because you only add incrSizeOfReturnBuffer to the current size).

After that, you will then try to copy 80 bytes from outLine into your 15 byte buffer:

snprintf(&tReturnString[curPosOutBuffer], lenOutLine+1, "%s\n", outLineBuffer);

You need to change your reallocation amount to ensure that it is at least enough to hold the length of the line you are trying to append. For example:

if (remainingLenOutBuffer < 0) {
    int neededSize = -remainingLenOutBuffer;

    if (neededSize < incrSizeOfReturnBuffer)
        neededSize = incrSizeOfReturnBuffer;
    newSize = sizeOfReturnBuffer + neededSize;
    // ...

Style issues

  • Typically, constants that are #defined are in capital letters and in snake_case. So I would change:

    #define initialSizeOfReturnBuffer 10
    

    to

    #define INITIAL_SIZE_OF_RETURN_BUFFER 10
    
  • There's no need to use sizeof(char). It's always 1.

\$\endgroup\$
3
  • \$\begingroup\$ I modified the code to have proper uppercase names for #defined constants. I was aware of the "bug" but for testing purposes I didn't want to use very long strings (see the comments next to INITIAL_SIZE_OF_RETURN_BUFFER and INCR_SIZE_OF_RETURN_BUFFER - in the production code that problem would not exist). I used the sizeof(char) because in the (old) notes of a C programming course at the Technical Univeristy of Vienna, I once took years ago, it was recommended. So it would be easier if somebody later wanted to use wchar_t. \$\endgroup\$
    – thgr
    Dec 12, 2015 at 16:27
  • \$\begingroup\$ On my original post on stackoverflow (didn't know about codereview) I got the following hint by Paul Ogilvie which I'd like to discuss here further: >>Check your frequent use of strlen, which every time goes through all the (many) characters to count the length - use information about its length, for example maintain a char *outLineBufPtr. Also use strcat to append \n to outLineBuffer instead of using the expensive vsnprintf function, or just copy the char manually, as *outLineBufPtr++ ='\n';<< I'm not very familiar with pointer programming - where would I start / what do I have to do? \$\endgroup\$
    – thgr
    Dec 12, 2015 at 16:39
  • 1
    \$\begingroup\$ @thgr I added an example of how you would properly resize your buffer. \$\endgroup\$
    – JS1
    Dec 12, 2015 at 18:22
1
\$\begingroup\$

Revised Version

Please find the revised version of my code below. The following things were improved:

  • Buffer size problem and style issues for #defines fixed (see JS1s answer).
  • Added a long string in main to test buffer realloc.
  • Return codes 'unix style': 12 (ENOMEM) or 0 (OK) is returned
  • Added a fputs replacement (tPuts).
  • Used #define preprocessor statements to use tprntf instead of printf and tPuts instead of fputs.
  • Added tFreeMemory to free allocated memory.
  • strlen performance improvement: just parse new part of outLineBuffer - thanks Paul Ogilvie.
  • Uploaded a complete Xcode 7 project on github swift-C-string-passing . The gcc standalone version can be found there too.

C Code

// C hack: replace printf to collect output and return complete string by using
// a line buffer.
// Beware of calling tprntNewLine so the line is added to the return string!

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

#define OK 0      // linux return value: 0 = successful
#define ENOMEM 12 // linux return value: 12 = out of memory

// outLineBuffer collects one output line by several calls to tprntf
#define INITIAL_SIZE_OF_RETURNBUFFER 10 // reduced for tests (would be 16*1024)
#define INCR_SIZE_OF_RETURNBUFFER 5 // reduced for testing (would be 1024*1024)
#define OUTLINE_BUFFER_MAXSIZE 4095
char outLineBuffer[sizeof(char)*OUTLINE_BUFFER_MAXSIZE] = "";
char *tReturnString;
size_t sizeOfReturnBuffer, curPosOutBuffer = 0, lenOutLine = 0;

With the replacement tprntf and tPust for all the original printf´ andfputs`:

// fputs replacement to collect the parts of one output line in outLineBuffer.
static int tPuts(const char *s, FILE *stream)
{
    const size_t maxLen = sizeof(char)*OUTLINE_BUFFER_MAXSIZE;
    int rVal;

    if (stream == stdout) {
        rVal = snprintf (&outLineBuffer[lenOutLine], maxLen-lenOutLine, "%s",s);
        lenOutLine += strlen(&outLineBuffer[lenOutLine]);
        if (rVal < 0) {
            return EOF;
        } else {
            return rVal;
        }
    } else {
        return fputs(s, stream);
    }
}

// fputs replacement to collect the parts of one output line in outLineBuffer.
static int tPuts(const char *s, FILE *stream)
{
    const size_t maxLen = sizeof(char)*OUTLINE_BUFFER_MAXSIZE;
    int rVal;

    if (stream == stdout) {
        rVal = snprintf (&outLineBuffer[lenOutLine], maxLen-lenOutLine, "%s",s);
        lenOutLine += strlen(&outLineBuffer[lenOutLine]);
        if (rVal < 0) {
            return EOF;
        } else {
            return rVal;
        }
    } else {
        return fputs(s, stream);
    }
}

And the function when we complete one output line (everywhere \n is printed, don´t forget to call this otherwise the line won´t show up):

// Output line is now complete: copy to return buffer and reset line buffer.
//   Don't forget to call this (especially for the last prints) so the line 
//   is added to the return string!
static void tprntNewLine()
{
    size_t newSize;
    long remainingLenOutBuffer, neededSize;
    char *newOutBuffer;

    remainingLenOutBuffer = sizeOfReturnBuffer-curPosOutBuffer-1;
    lenOutLine++; // + newline character (\n)
    remainingLenOutBuffer -= lenOutLine;

    if (remainingLenOutBuffer < 0) {
        //newSize = sizeOfReturnBuffer + sizeof(char)*INCR_SIZE_OF_RETURNBUFFER;
        neededSize = -remainingLenOutBuffer;
        if (neededSize < sizeof(char)*INCR_SIZE_OF_RETURNBUFFER)
            neededSize = sizeof(char)*INCR_SIZE_OF_RETURNBUFFER;
        newSize = sizeOfReturnBuffer + neededSize;

        if ((newOutBuffer = realloc(tReturnString, newSize)) != 0) {
            tReturnString = newOutBuffer;
            sizeOfReturnBuffer = newSize;
        } else {
            // just write part that is still available:
            lenOutLine += remainingLenOutBuffer;
            //remainingLenOutBuffer = 0;
        }
    }

    snprintf(&tReturnString[curPosOutBuffer],lenOutLine+1,"%s\n",outLineBuffer);

    curPosOutBuffer += lenOutLine;
    outLineBuffer[0] = 0;
    lenOutLine = 0;
}

Free allocated memory:

void tFreeMemory ()
{
    free(tReturnString);
}

And a little main to test it (without Swift - e.g. plain gcc):

#ifndef COLLECT_STDOUT_IN_BUFFER
#define COLLECT_STDOUT_IN_BUFFER
#define printf tprntf
#define fputs tPuts
#endif

// For testing with C compiler. Rename when used in Xcode project e.g. to mymain
int main(int argc, char *argv[])
{
    int i;
    sizeOfReturnBuffer = INITIAL_SIZE_OF_RETURNBUFFER*sizeof(char);
    if ((tReturnString = malloc(sizeOfReturnBuffer)) == 0) {
        // "Sorry we are out of memory. Please close other apps and try again!"
        return ENOMEM; 
    }
    tReturnString[0] = 0;
    curPosOutBuffer = 0;

    for (i = 0; i < argc; i++) printf("%s ", argv[i]);
    tprntNewLine();

    printf("%s", "ABC\t");
    printf("%d", 12);
    tprntNewLine(); // enough space for that ;-)
    fputs("DEF\t", stdout);
    printf("%d", 34);
    tprntNewLine(); // realloc necessary ...
    printf("%s", "xxxxxxxxx 80 chars are way more than the current buffer "
             "could handle! xxxxxxxxxx\t");
    printf("%d", 56);
    tprntNewLine(); // again realloc (test: too small INCR_SIZE_OF_RETURNBUFFER)

#ifdef COLLECT_STDOUT_IN_BUFFER //undo rename to view results:
#undef printf
#endif
    printf("tReturnString at the end:\n>%s<\n", tReturnString);
    tFreeMemory ()
    return OK;
}

For the swift interaction please have a look at github swift-C-string-passing

\$\endgroup\$
5
  • 1
    \$\begingroup\$ I strongly disagree with you accepting your own answer. All you have provided is the updated code; you have not given a review. Right now, your best answer is the one by JS1 and that is the answer that should be accepted. \$\endgroup\$
    – SirPython
    Dec 20, 2015 at 2:59
  • \$\begingroup\$ @SirPython my question was: It works - but as I am fairly new in "real" programming - i.e. C and Apples "brandnew" Swift - am not sure wheter this is a good solution. Is it? And if not - what would you suggest? I incorporated all the feedback (e.g. by Paul Ogilvie and JS1) into my answer. But there was more (read the list, please). And the best (and complete) answer to my question is now -- my answer. Or can you help any further (e.g. with feedback if that will be accepted by the app store guys, etc ...)? If you downvoted the answer: Can you please point out what part of the code is bad? \$\endgroup\$
    – thgr
    Dec 20, 2015 at 17:46
  • \$\begingroup\$ Mainly, I disagree with the fact that you accepted your own answer of only updated code. I can remove my down-vote, but you'd have to edit your post. \$\endgroup\$
    – SirPython
    Dec 20, 2015 at 22:57
  • \$\begingroup\$ Fair enough. I think I understand your point now. I would remove my down-vote, but the site will not let me unless your post has been edited. \$\endgroup\$
    – SirPython
    Dec 21, 2015 at 21:30
  • \$\begingroup\$ Why are you using snprintf(&outLineBuffer[lenOutLine], maxLen-lenOutLine, "%s",s);? It would be better to do strlcpy(&outLineBuffer[lenOutLine], s, maxLen-lenOutLine); or strncat(outLineBuffer, s, maxLen-lenOutLine-1); to concatenate a string that way. \$\endgroup\$
    – JustinCB
    Jun 17, 2018 at 16:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.