[m-rev.] IO error type

Julien Fischer jfischer at opturion.com
Thu Aug 18 14:11:05 AEST 2022


Hi Peter,

On Thu, 18 Aug 2022, Peter Wang wrote:

> On Wed, 17 Aug 2022 19:31:26 +1000 Julien Fischer <jfischer at opturion.com> wrote:
>>
>>
>> On Wed, 17 Aug 2022, Peter Wang wrote:
>>
>>>    :- type system_error.
>>>
>>>    :- func make_io_error(system_error, string) = io.error.
>>>
>>>    :- pred get_system_error(io.error::in, system_error::out)
>>>        is semidet.
>>>
>>> That is, to introduce a backend/platform specific system_error type,
>>> and add a field for system_errors in the io.error type.
>>> (It would still be possible to construct an io.error without a
>>> system_error value.)
>>
>> That was my preferred approach as well, however on Windows there is
>> samll wrinkle with this: some of the system error codes we return are
>> *not* errno values but instead are system error codes (e.g. of the kind
>> returned by GetLastError()). So at least on Windows, if you embed a raw
>> system error in an io.error, you have the additional problem of having
>> to know what kind of error code it is.
>
> Can we reserve a bit in the system_error? Otherwise we need another
> field in io.error, which is fine as well.

My proposal is as follows:

1. We keep the definition of the type system_error/0 as it is, namely:

     :- type system_error.
     :- pragma foreign_type(c, system_error, "MR_Integer").
     :- pragma foreign_type("C#", system_error, "System.Exception").
     :- pragma foreign_type(java, system_error, "java.lang.Exception").

(Actually, the Java version probably ought to be java.lang.Throwable,
but that's a different matter.)

2. We export system_error/0 in the documented interface of the io module
    and also document what its foreign representation is for each backend.

3. We redefine io.error to be like the following:

     :- type io.error
         --->    io_error_string(string)    % for use by make_io_error/1.
 	;       io_error_errno(string, system_error)
         ;       io_error_win_error(string, system_error)
         ;       io_error_csharp_java_error(string, system_error).

(At the point we construct these values we will know whether an "int"
error is an errno or Windows error code.)

Since io.error now contains the system error we could also omit the
string in the latter threee cases and only construct it if the user
requests it.

4. We can add various semidet functions for classifying and retrieving
the system_error value, e.g.

      :- pred io.get_errno_error(io.error::in, system_error::out)
          is semidet.
      :- pred io.get_windows_error(io.error::in, system_error::out)
          is semidet.

etc. etc.  This provides a mechanism for third-party libraries to get
the raw error code / exception (and know what kind of error code it is).

5. We also add the functions you listed below

>>>    :- pred get_error_name(io.error::in, string::out)
>>> 	is semidet.
>>>
>>> 	% As above, but takes a system_error instead.
>>> 	%
>>>    :- pred get_system_error_name(system_error::in, string::out)
>>> 	is semidet.

except that the signature of the latter one would be:

     :- pred get_system_error_name(io.error::in, string::out)
        is semidet.

(I hope there's a way of getting Windows error code name, that doesn't
involve switching over all 5000 or so Windows error codes; perhaps
FormatMessage can do that?)

>> (This is not a problem with the existing code, since all error codes
>> are turned into error messages fairly close to the point where they
>> occur, and at those points we know what kind of error code it is.)
>>
>> Looking a bit closer: the calls to GetLastError() occur pretty much
>> in the dir module, so maybe we can re-arrange how it handles errors
>> to avoid that?
>
> Someone may want to implement more of the standard library using native
> Windows APIs. It would be worse for maintainability but it is (or was?)
> necessary to access longer path names; not sure if there are other
> benefits.

We should preserve the possibility that parts of the io module can call
the native Windows API.

>>
>>> If we add your suggestion to allow converting an io.error to a name,
>>> we might have:
>>>
>>> 	% Returns a standard error name if the given error corresponds
>>> 	% to a system error known to the Mercury system, e.g. "ENOENT"
>>> 	% (or possibly the name of a Java or C# exception?).
>>
>> Given that C# and Java exceptions are usually arranged in a class
>> hierarchy, which name would you return?
>
> I was thinking the class name of the exception object, e.g.
> for C#, e.GetType().Name returns "FileNotFoundException"
> for Java, e.getClass().getName() returns "java.io.FileNotFoundException"

That seems fine.

>>> 	% Fails if the error is not a system error, or if the error is
>>> 	% not one recognised by the Mercury system.
>>> 	%
>>>    :- pred get_error_name(io.error::in, string::out)
>>> 	is semidet.
>>>
>>> 	% As above, but takes a system_error instead.
>>> 	%
>>>    :- pred get_system_error_name(system_error::in, string::out)
>>> 	is semidet.
>>
>> My preference would be for the standard library to simply give access
>> the the system error value and leave it for other system-specific
>> libraries (e.g. the POSIX binding) to provide further facilities for
>> examining it.
>
> I'd rather not pull in the posix library for various reasons:
>  - using anything from extras is a bit of a pain, not worth it for
>    small projects
>  - not sure about the code quality
>  - breaks compilation on other platforms/grades

I would prefer not too push large chunks of OS-specific stuff into the
standard library: that doesn't improve portability, it just means
programmers have to program around the bits their system doesn't
support. Examining the different kinds of errors produced by system
calls is intrinsically a OS-specific thing. The standard library can
only paper over the difference so far.

Julien.


More information about the reviews mailing list