9
\$\begingroup\$

As an exercise, I decided to try implementing a Mandelbrot Set viewer that produces ASCII images.

Small Example:

                                                           ..                                       
                                                          .......                                   
                                                         ...........                                
                                                        .............                               
                                                       ..............                               
                                                   ..........###.......                             
                                               ............######!........                          
                                            ...............%#####...............                    
                                          ...........!!.!.%#######..!..%.........                   
                                        .........##.%###################!..#.....                   
                                      ............###########################.....                  
                        ......   ...........!...############################!.....                  
                      ........................###############################......                 
                     .......................####################################!..                 
                    ........#....#.........####################################!...                 
                   .........!#########....!#####################################!..                 
              ............##############%.#####################################...                  
         ................!###############.####################################....                  
   ...................#######################################################.....                  
   ...................#######################################################.....                  
         ................!###############.####################################....                  
              ............##############%.#####################################...                  
                   .........!#########....!#####################################!..                 
                    ........#....#.........####################################!...                 
                     .......................####################################!..                 
                      ........................###############################......                 
                        ......   ...........!...############################!.....                  
                                      ............###########################.....                  
                                        .........##.%###################!..#.....                   
                                          ...........!!.!.%#######..!..%.........                   
                                            ...............%#####...............                    
                                               ............######!........                          
                                                   ..........###.......                             
                                                       ..............                               
                                                        .............                               
                                                         ...........                                
                                                          .......                                   
                                                           ..  

Huge Example (You may want to zoom out.)

I ended up playing around with a few new things here:

  • I decided to try limiting my use of pointers to only the cases where it's absolutely necessary (like set_complex). I decided cleanliness was better than performance, and it seems to perform well anyways; even though it's using copies everywhere.

  • I opted to use snake_case, as that seems to be semi-idiomatic for C, and is far more readable than camelCase or lowercase.

  • I'm doing (very simple) file operations for the first time.

  • I decided to wrap malloc and calloc in terminating_ wrapper functions that handle checking the returned pointer; terminating with a message if it's NULL.

I'd like thoughts on anything here, though I'm especially interested in:

  • Is there a better way of writing char_for_iters? The bulky branching seems less than ideal.

  • I've gotten a few suggestions that I should be using 1 instead of sizeof(char). Is this really necessary/preferred though? I find I like the explicitness of having the type specified; even if it's not necessary.

  • Are my terminating_ malloc and calloc functions at all a common idea? Or is it far more typical to have the handling inline in the code?



helpers.h

#ifndef HELPERS_H
#define HELPERS_H

#include <stdlib.h>

// Prints an error message to stderr if ptr is NULL
// Message is in the form "Could not allocate space for %s.".
void ensure_allocation(const void* ptr, const char* allocation_reason);

// Attempts to allocate the requested amount of memory and asserts the validity of the
//  returned pointer using ensure_allocation before returning
void* terminating_malloc(size_t bytes, const char* allocation_reason);
void* terminating_calloc(size_t count, size_t bytes_per, const char* allocation_reason);

#endif

helpers.c

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

#include "helpers.h"

void ensure_allocation(const void* ptr, const char* allocation_reason) {
    if (!ptr) {
        fprintf(stderr, "Could not allocate space for %s.", allocation_reason);
        exit(EXIT_FAILURE);
    }
}

void* terminating_malloc(size_t bytes, const char* allocation_reason) {
    void* ptr = malloc(bytes);

    ensure_allocation(ptr, allocation_reason);

    return ptr;
}

void* terminating_calloc(size_t count, size_t bytes_per, const char* allocation_reason) {
    void* ptr = calloc(count, bytes_per);

    ensure_allocation(ptr, allocation_reason);

    return ptr;
}

complex.h

#ifndef COMPLEX_H
#define COMPLEX_H

typedef struct Complex {
    double real;
    double imaginary;

} Complex;

void set_complex(Complex* c, double real, double imaginary);
Complex new_complex(double real, double imaginary);
Complex copy_complex(Complex src);

// Overwrites out with the result of squaring c.
Complex square_complex(Complex c);

#endif

complex.c

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

#include "complex.h"
#include "helpers.h"

// TODO: This should probably all be moved to the header for performance reasons

void set_complex(Complex* c, double real, double imaginary) {
    c->real = real;
    c->imaginary = imaginary;
}

Complex new_complex(double real, double imaginary) {
    Complex c;
    set_complex(&c, real, imaginary);

    return c;
}

Complex copy_complex(Complex src) {
    Complex copy;
    copy.real = src.real;
    copy.imaginary = src.imaginary;

    return copy;
}

Complex square_complex(Complex c) {
    double real = (c.real * c.real) - (c.imaginary * c.imaginary);
    double imaginary = 2 * c.real * c.imaginary;

    return new_complex(real, imaginary);
}

iteraton.h

#ifndef ITERATION_H
#define ITERATION_H

#include <stdlib.h>

#include "complex.h"

// Can be lowered to sacrifice accuracy for speed
#define STD_MAX_ITERATIONS 200

// Once this is exceeded, a number is bound to head off into infinity
#define STD_INFINITY_LIMIT 2 

// Test how many iterations it takes for c to go to infinity when iterated
size_t test_point(Complex c, size_t max_iteration, size_t infinity_limit);

#endif

iteration.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#include "iteration.h"
#include "complex.h"

// Returns the result of iterating current_complex once using the standard Mandelbrot iteration method
static Complex mandelbrot_iteration(
    Complex initial_complex,
    Complex current_complex) {

    Complex sqrd = square_complex(current_complex);

    return new_complex(
                sqrd.real + initial_complex.real,
                sqrd.imaginary + initial_complex.imaginary);
}

static bool is_under_limit(Complex c, size_t infinity_limit) {
    // Numbers must be positive as they're being squared.
    return (size_t)((c.real * c.real) + (c.imaginary * c.imaginary))
                    <= (infinity_limit * infinity_limit);
}

size_t test_point(Complex c, size_t max_iteration, size_t infinity_limit) {
    Complex current_c = copy_complex(c);

    size_t i;
    for (i = 0; i < max_iteration; i++) {
        if (is_under_limit(current_c, infinity_limit)) {
            current_c = mandelbrot_iteration(c, current_c);

        } else {
            break;
        }
    }

    return i;
}

display is the "text image" specific portion of the code. When/if I adapt the code to produce actual images, I'd write a separate display file to produce images instead of text.

display.h

#ifndef DISPLAY_H
#define DISPLAY_H

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

// Produces a formatted string representing a view of the Mandelbrot Set
char* format_mandelbrot_view(double lower_real,
                             double upper_real,
                             double lower_imag,
                             double upper_imag,
                             size_t chars_wide,
                             size_t chars_high);

// Prints a view returned by format_mandelbrot_view to the given file stream
void print_mandelbrot_view(FILE* stream,
                           double lower_real,
                           double upper_real,
                           double lower_imag,
                           double upper_imag,
                           size_t chars_wide,
                           size_t chars_high);

#endif

display.c

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

#include "helpers.h"
#include "iteration.h"

static char char_for_iters(size_t iters) {
    if (iters >= 200) {
        return '#';

    } else if (iters >= 150) {
        return '@';

    } else if (iters >= 100) {
        return '%';

    } else if (iters >= 50) {
        return '!';

    } else if (iters >= 5) {
        return '.';

    } else {
        return ' ';
    }
}

char* format_mandelbrot_view(double lower_real,
                             double upper_real,
                             double lower_imag,
                             double upper_imag,
                             size_t chars_wide,
                             size_t chars_high) {

    size_t buffer_size = (chars_wide * chars_high) + chars_high + 1;
    char* buffer = terminating_calloc(buffer_size, sizeof(char), "complex format buffer");

    double comp_width = upper_real - lower_real;
    double comp_height = upper_imag - lower_imag;

    double real_step = comp_width / (chars_wide - 1);
    double imag_step = comp_height / (chars_high - 1);

    size_t i = 0;
    for (double y = lower_imag; y <= upper_imag && i < buffer_size - 1; y += imag_step) {
        for (double x = lower_real; x <= upper_real && i < buffer_size - 1; x += real_step) {
            size_t iters = test_point(new_complex(x, y),
                                      STD_MAX_ITERATIONS, STD_INFINITY_LIMIT);

            buffer[i] = char_for_iters(iters);

            i++;
        }

        buffer[i] = '\n';

        i++;
    }

    return buffer;
}

void print_mandelbrot_view(FILE* stream,
                           double lower_real,
                           double upper_real,
                           double lower_imag,
                           double upper_imag,
                           size_t chars_wide,
                           size_t chars_high) {

    char* formatted = format_mandelbrot_view(lower_real, upper_real,
                                             lower_imag, upper_imag,
                                             chars_wide, chars_high);

    fprintf(stream, "%s\n", formatted);

    free(formatted);
}

main.c

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

#include "display.h"

#define OUTPUT_PATH "./mandelbrot_output.txt"

void save_view_at(double lower_real,
                  double upper_real,
                  double lower_imag,
                  double upper_imag,
                  size_t image_width) {

    FILE* file = fopen(OUTPUT_PATH, "w+");

    if (file) {
        print_mandelbrot_view(file,
                              lower_real, upper_real,
                              lower_imag, upper_imag,
                              // Halving the height because it looks best when
                              //  width is 2 * height.
                              image_width, (size_t)(image_width / 2));

        fclose(file);

    } else {
        printf("Cannot open file at %s", OUTPUT_PATH);

    }

}

int main() {
    save_view_at(-2, 1, -1.5, 1.5, 500);

    printf("Saved...\n");

    return 0;
}
\$\endgroup\$
  • \$\begingroup\$ I just realized that copy_complex can simply return the parameter since I only need a shallow copy anyways. Is this at all typical? \$\endgroup\$ – Carcigenicate Apr 17 at 12:19
  • 2
    \$\begingroup\$ c has complex numbers which might be helpful: en.cppreference.com/w/c/numeric/complex \$\endgroup\$ – sudo rm -rf slash Apr 20 at 21:22
3
\$\begingroup\$

The "terminating" allocators work well for small programs like this; in larger projects or libraries, we want to do something better than terminate the program when allocation fails. A common naming scheme (perhaps taken from Perl) is malloc_or_die() - that's slightly clearer about the behaviour. It's usual to end your error message (and indeed program output generally) with a newline:

    fprintf(stderr, "Could not allocate space for %s.\n", allocation_reason);

I'm not convinced that ensure_allocation() should be part of the interface of "helpers.h" - it could equally be a static-linkage function in the implementation.

I'm surprised you're rolling your own complex number type rather than using the standard complex numbers introduced by C99.

new_complex() and copy_complex() are inconsistent in their approach: the former uses set_complex() to assign to the members, but the latter assigns directly. Both styles work, but it's easier to read if they're consistent. Alternatively, implement copy in terms of new:

Complex copy_complex(Complex src) {
    return new_complex(src.real, src.imaginary);
}

Consider, though:

Complex copy_complex(Complex src) {
    return src;
}

In other words, we can use plain = instead of calling a function.

It's not clear why the infinity_limit should be an integer type. Since it's a limit on the magnitude of a floating-point value, it makes more sense for it simply to be a double itself and remove the cast. I measured no impact on speed with this change.

When calling calloc() to create an array, it's better to use an actual element as argument to sizeof, rather than repeating the type name:

char* buffer = terminating_calloc(buffer_size, sizeof *buffer, "complex format buffer");

That way, if we ever change the type of the buffer, we only have one place that needs to change.

We could avoid allocating the buffer at all, if we calculate each character and print immediately. That obviously requires a little restructuring of the functions, but is better than returning a pointer that the caller must free.

I don't like the hard-coded output file name; I changed the code in my copy to print to stdout instead, so the user can redirect to any file.

No need to cast when dividing image_width by two - the result is also size_t.

The error messages ought to go to stderr, rather than stdout (this was correct in ensure_allocation(), so must just be an oversight in main.c).

Pedantry: int main(void) to make the declaration a prototype, not int main().

\$\endgroup\$
  • 1
    \$\begingroup\$ Thank you. I'm temporarily moved on from C, but when I return I'll go over this again. \$\endgroup\$ – Carcigenicate Sep 16 at 18:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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