Swift, libpng and error handling

Hi, I'm working on a Swift wrapper to libpng mostly for my own learning experience and I've gotten quite a bit working.

I would like the code to fail more gracefully though. The default behavior, and the only behavior I've been able to recreate, is a program exiting crash when libpng reaches an error.

In an attempt to side step writing helper functions in C to work with setjmp/longjmp, I decided to try using custom error callbacks in the png_create_write_struct function.

The problem is that libpng just works really well when it can get in and out of dodge with a jumpdef, but I'm not sure how to exit the parent process from a C callback without bringing down the whole house in Swift?

Is that even possible? The code below works as expected, but I'd like to figure out how to do something better than exit() or abort() from a callback.

A heads up that this can't possibly work like this would also be helpful and I'll try a different approach/move on. Thanks in advance.

#if os(Linux)
import Glibc
#else
import Darwin
#endif

import Foundation
import png //<- package wrapper around C library. In the linked repo.


public struct SwiftLIBPNG {
    
    public init() {}
    
    
    //MARK: Global Error Callbacks
    
    struct PNGErrorInfo {
        var png_ptr:OpaquePointer?
        var info_ptr:OpaquePointer?
        var fileHandle:UnsafeMutablePointer<FILE>?
        var testExtraData:UInt32
        
        func print_info() {
            print("\(String(describing: png_ptr)), \(String(describing: info_ptr)), \(String(describing: fileHandle)), \(testExtraData)")
        }
    }

    static let writeErrorCallback:@convention(c) (Optional<OpaquePointer>, Optional<UnsafePointer<CChar>>) -> () = { png_ptr, message in
        if let error_ptr = png_get_error_ptr(png_ptr) {
            print("There was a non nil error pointer set at \(error_ptr)")
            var typed_error_ptr = error_ptr.load(as: PNGErrorInfo.self)//error_ptr.assumingMemoryBound(to: PNGErrorInfo.self)
            typed_error_ptr.print_info()
            //If aborting whole program everything should be freed automatically, but in case not...
            precondition(png_ptr == typed_error_ptr.png_ptr)
            png_destroy_write_struct(&typed_error_ptr.png_ptr, &typed_error_ptr.info_ptr)
            if typed_error_ptr.fileHandle != nil {
                fclose(typed_error_ptr.fileHandle)
            }
        }
    
        if let message {
            print("libpng crashed with warning: \(String(cString: message))")
        } else {
            print("libpng crashed without providing a message.")
        }
        
        //Some way to kill png?
        //How to leave PNG write...
        exit(99)  //see also https://en.cppreference.com/w/c/program/atexit
        //abort() //terminates the process by raising a SIGABRT signal, possible handler?
        //This function MUST NOT go back to the parent caller. Getting here means that code cannot reasonably proceed. 

        
    }
    
    static let writeWarningCallback:@convention(c) (Optional<OpaquePointer>, Optional<UnsafePointer<CChar>>) -> () = { png_ptr, message in
        if let error_ptr = png_get_error_ptr(png_ptr) {
            print("There was a non nil error pointer set at \(error_ptr)")
            
        }
        if let message {
            print("libpng sends warning: \(String(cString: message))")
        } else {
            print("libpng sends unspecified warning")
        }
        
        //Use the error pointer to set flags, etc.

    }
    
    
}

extension SwiftLIBPNG {
    // EXAMPLE USAGE
    //    func writeImage() {
    //        let width = 5
    //        let height = 3
    //        var pixelData:[UInt8] = []
    //
    //        for _ in 0..<height {
    //            for _ in 0..<width {
    //                pixelData.append(0x77)
    //                pixelData.append(0x00)
    //                pixelData.append(UInt8.random(in: 0...UInt8.max))
    //                pixelData.append(0xFF)
    //            }
    //        }
    //
    //        let data = try? SwiftLIBPNG.buildSimpleDataExample(width: 5, height: 3, pixelData: pixelData)
    //        if let data {
    //            for item in data {
    //                print(String(format: "0x%02x", item), terminator: "\t")
    //            }
    //            print()
    //
    //            let locationToWrite = URL.documentsDirectory.appendingPathComponent("testImage", conformingTo: .png)
    //            do {
    //                try data.write(to: locationToWrite)
    //            } catch {
    //                print(error.self)
    //            }
    //        }
    //    }
    
    //NOT using "libpng simplified API"
    //takes a width, height and pixel data in RR GG BB AA byte order
    public static func buildSimpleDataExample(width:UInt32, height:UInt32, pixelData:[UInt8]) throws -> Data {
        var pixelsCopy = pixelData //Could have been an inout
        
        //-----------------------------  INTENTIONAL ERROR
        let bitDepth:UInt8 = 1 //should be 8 (1 byte, values 1, 2, 4, 8, or 16) (has to be 8 or 16 for RGBA)
        //-----------------------------  END INTENTIONAL ERROR
        let colorType = PNG_COLOR_TYPE_RGBA //UInt8(6), (1 byte, values 0, 2, 3, 4, or 6) (6 == red, green, blue and alpha)
        
        
        var pngIOBuffer = Data() //:[UInt8] = [] // //
        withUnsafePointer(to: pngIOBuffer) { print("io buffer declared: \($0)") }
        
        var pngWriteErrorInfo = PNGErrorInfo(testExtraData: 42)
        
        var png_ptr:OpaquePointer? = png_create_write_struct(PNG_LIBPNG_VER_STRING, &pngWriteErrorInfo, writeErrorCallback, writeWarningCallback)
        if (png_ptr == nil) { throw PNGError.outOfMemory }
        
        //Makes the pointer to handle information about how the underlying PNG data needs to be manipulated.
        //C:-- png_create_info_struct(png_const_structrp!)
        var info_ptr:OpaquePointer? = png_create_info_struct(png_ptr);
        if (info_ptr == nil) {
            png_destroy_write_struct(&png_ptr, nil);
            throw PNGError.outOfMemory;
        }
        
        pngWriteErrorInfo.fileHandle = nil
        pngWriteErrorInfo.png_ptr = png_ptr
        pngWriteErrorInfo.info_ptr = info_ptr
        
        let writeDataCallback: @convention(c) (Optional<OpaquePointer>, Optional<UnsafeMutablePointer<UInt8>>, Int) -> Void = { png_ptr, data_io_ptr, length in
            guard let output_ptr:UnsafeMutableRawPointer = png_get_io_ptr(png_ptr) else { return }
            guard let data_ptr:UnsafeMutablePointer<UInt8> = data_io_ptr else { return }
            //print("callback io output buffer: \(output_ptr)")
            //print("callback io data buffer: \(data_ptr)")
            
            let typed_output_ptr = output_ptr.assumingMemoryBound(to: Data.self)
            typed_output_ptr.pointee.append(data_ptr, count: length)
        }
        
        png_set_write_fn(png_ptr, &pngIOBuffer, writeDataCallback, nil)
        
        
        // THIS IS WHERE THE INTENTIONAL ERROR WILL FAIL
        //---------------------------------------------------------------- IHDR
        png_set_IHDR(png_ptr, info_ptr, width, height,
                     Int32(bitDepth), colorType,
                     PNG_INTERLACE_NONE,
                     PNG_COMPRESSION_TYPE_DEFAULT,
                     PNG_FILTER_TYPE_DEFAULT
        )
    
        //---------------------------------------------------------------  IDAT
        pixelsCopy.withUnsafeMutableBufferPointer{ pd_pointer in
            var row_pointers:[Optional<UnsafeMutablePointer<UInt8>>] = []
            for rowIndex in 0..<height {
                let rowStart = rowIndex * width * 4
                row_pointers.append(pd_pointer.baseAddress! + Int(rowStart))
            }
            
            //png_set_rows(png_ptr: png_const_structrp!, info_ptr: png_inforp!, row_pointers: png_bytepp!)
            png_set_rows(png_ptr, info_ptr, &row_pointers)
            
            png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nil)
        }
        
        //--------------------------------------------------------   PNG CLEANUP
        png_destroy_write_struct(&png_ptr, &info_ptr);
        //---------------------------------------------------------------------
        
        return pngIOBuffer
    }
    
}

For what it's worth I've decided to go ahead and add a C->Swift interface layer written in C in the style of this example code from libpng to get some smaller functions that will return, each with their own little jmpdef set.

Not written yet, but I'll update when I get it working, probably next week. (fingers crossed.)

So, the basic model of what I've done is to write a C function that wraps the calls to libpng and sets the long jump definitions accordingly, which will return a wrapper-developers-choice of int if something goes wrong, 0 if all okay. e.g.

int pngb_set_IHDR(png_structp png_ptr, png_infop info_ptr, png_uint_32 width, png_uint_32 height, int bit_depth, int color_type, int interlace_method, int compression_method, int filter_method) {
    
    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_write_struct(&png_ptr, &info_ptr);
        return 2;
    }
    
    png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type, interlace_method, compression_method, filter_method);
    
    return 0;
}

There is then a companion Swift function that throws the appropriate error:

    static func setIHDR(png_ptr:OpaquePointer, info_ptr:OpaquePointer, width:UInt32, height:UInt32,
                        bitDepth:Int32, colorType:Int32) throws {
        let result = pngb_set_IHDR(png_ptr, info_ptr, width, height, bitDepth, colorType,                     
                                   PNG_INTERLACE_NONE,
                                   PNG_COMPRESSION_TYPE_DEFAULT,
                                   PNG_FILTER_TYPE_DEFAULT)
        if result != 0 {
            //PNGError implemented with an init that takes a code.
            throw PNGError(result) 
        }
    }

It means lots of boiler plate for almost every libpng function call (just the ones that can fail), and a slow down for all the code checking.

If there is a frequently bundled together set of tasks for libpng to do, it would be better to write the implementation in C, and then have the one Swift wrapper for that one bigger function, if speed is of the essence.

This code is now on the main branch. Custom C in CBridgePNG target.

Comments on architecture, possible improvements still welcome and appreciated.