I am slowly wrapping a C-library in Julia. The c-library often gives a resultcode which indicates success or various problems. I am wondering what the preferred way of dealing with the resultcode is.
Pass the resultcode to the calling program for it to either ignore or deal with it.
If the resultcode indicates success, ignore it, otherwise print an error message
Both 1 & 2
Another option?
So far I have taken approach 2. I am wondering if I should transition to option 3 or a modified version of it. I suppose there may be times when the calling program might have logic that depends upon the resultcode.
Before going further, I though I would ask the community for which option is the best practice.
An example code snippet exhibiting option 3 behaviour follows:
function mcc172_firmware_version(address::Integer)
version = Ref{UInt16}()
resultcode = ccall((:mcc172_firmware_version, libdaqhats),
Cint, (UInt8, Ref{Cushort}), address, version)
printerror(resultcode)
return (resultcode, version[])
end
with the printerror function causing an error if the resultcode is not successful, which may not be the ideal either.
function printerror(resultcode)
# map resultcode to descriptive string
resultDict = Dict{Int32, String}(
0 => "Success, no errors",
-1 => "A parameter passed to the function was incorrect.",
-2 => "The device is busy.",
-3 => "There was a timeout accessing a resource.",
-4 => "There was a timeout while obtaining a resource lock.",
-5 => "The device at the specified address is not the correct type.",
-6 => "A needed resource was not available.",
-7 => "Could not communicate with the device.",
-10 => "Some other error occurred.")
if resultcode != 0
# @show(resultcode)
error(resultDict[resultcode])
end
end
Another idea would be to create your own error type.
const MCC172ErrorDict = Dict{MCC172ErrorCode, String}(
success => "Success, no errors",
incorrect_parameter => "A parameter passed to the function was incorrect.",
device_busy => "The device is busy.",
resource_timeout => "There was a timeout accessing a resource.",
resource_lock_timeout => "There was a timeout while obtaining a resource lock.",
device_not_correct_type => "The device at the specified address is not the correct type.",
resource_not_available => "A needed resource was not available.",
no_device_communication => "Could not communicate with the device.",
other_error => "Some other error occurred."
)
struct MCC172Error <: Exception
id::MCC172Error
end
function Base.showerror(io::IO, e::MCC172Error)
# map resultcode to descriptive string
if resultcode != success
print(io, MCC172ErrorDict[e.id])
end
end
julia> throw(MCC172Error(incorrect_parameter))
ERROR: A parameter passed to the function was incorrect.
Stacktrace:
[1] top-level scope
@ REPL[39]:1
In my opinion, if the result code indicates that something actually exceptional has happened, throw an exception. But if it’s something that can be more or less expected to happen, don’t force the user to try/catch, pass on the result code instead.
I appreciate the more elegant methods of error handling. The C-program places the responsibility of dealing with the result code with the calling program. I have the option of putting this in the function that contains the ccall or I can push it off to the calling program.
Unless there is a preferred convention within the Julia community, I think what I will do is if the action to take with the result code is ambiguous I will pass on the result code, and if it is unambiguously an error, I will throw an exception like Gunnar suggests.
It may be useful to see an example at scale. HDF5 has some error handling API. We wrap that into a macro.
We have some code generation scripts which generate code that generates direct wrappers for each C binding:
We then have a second layer which creates helpers around a direct wrappings to make the functions more Julian. In this case, we want to return the version numbers directly rather than requiring that they be passed by reference.
That helps but now it does not display the error message. MWE
struct MyError <: Exception
code::Cint
end
const myerror_message = Dict{Cint, String}(
0 => "Success, no errors",
-1 => "A parameter passed to the function was incorrect.",
-2 => "The device is busy.",
-3 => "There was a timeout accessing a resource.",
-4 => "There was a timeout while obtaining a resource lock.",
-5 => "The device at the specified address is not the correct type.",
-6 => "A needed resource was not available.",
-7 => "Could not communicate with the device.",
-10 => "Some other error occurred.")
function Base.showerror(io::IO, e::MyError)
print(io, "MyError: ", myerror_message(e.code))
end
my_error(code::Integer) = code != 0 && throw(MyError(code))
With the error message
julia> my_error(-5)
ERROR:
Stacktrace:
[1] my_error(code::Int64)
@ Main .\REPL[6]:1
[2] top-level scope
@ REPL[7]:1
SYSTEM (REPL): showing an error caused an error
ERROR: MethodError: objects of type Dict{Int32, String} are not callable
The object of type `Dict{Int32, String}` exists, but no method is defined for this combination of argument types when trying to treat it as a callable object.
Stacktrace: