[m-users.] Ignoring !IO in an FFI binding, aceptable or hideously bad?

Zoltan Somogyi zoltan.somogyi at runbox.com
Mon Feb 17 19:22:15 AEDT 2025



On Sun, 16 Feb 2025 11:38:48 +0000, "Sean Charles (emacstheviking)" <objitsu at gmail.com> wrote:

> Hello,
> 
> I have a not insignificant binding to Raylib now, and during a recent bit of experimental coding, I decided on trying a different way to manage assets, in this case fonts.
> 
> :- import_module raylib.
> :- type font_state
>     --->    font_state(
>                 default :: rfont,
>                 main    :: rfont
>             ).
> :- mutable(fstate, maybe(font_state), no, ground, [untrailed]).
> 
> I am forced to initialise to `no` because until the Raylib library is initialised, loading fonts doesn't work,

This is the kind of question for which it is very hard to provide help.
The reason is explained in detail in the link named "the XY problem"
at https://mercurylang.org/contact.html, but in short: you are
asking about how to use mutables, but using mutables is never
anybody's *objective*; it is merely the means to reach their objective.
By not telling us enough details about your objective, it is impossible
for us to tell whether mutables are useful *at all* in reaching your
objective.

You do say that your objective is to load fonts from Raylib, but since
I for one know nothing about Raylib, or whether this loading is
intended to happen only once or many times, that does not help.
Specifically, is the scenario in your use case like this:

scenario A:
- initialise Raylib
- get some fonts from Raylib
- never touch fonts in Raylib again,

or is it like this,

scenario B:
- initialise Raylib
- get some fonts from Raylib
- use those fonts
- get some other fonts from Raylib to replace the first batch,
- use the updated fonts
- get some yet more fonts
- rinse and repeat

or is it some third scenarion that I cannot even guess at?

Note that solutions that are appropriate for scenario A
are not appropriate for B, and vice versa. And it is far from clear
that mutables need to be involved in the solution at all,
regardless of how you want to initialize that mutable.

For example, in the Mercury compiler, in most cases where
some pass relies on the values of some parameters (which may,
or may not, be how you intend to use your fonts), we simply put
all the parameters into a type of the form

:- type pass_x_params
    --->    type_x_params(
                   param_1 :: param_1_type,
                   param_2 :: param_2_type,
                   ...
               )

and pass values of that type to every predicate in that pass.

This technique and mutables alternative  approaches for doing in Mercury
what one would do with global variables in imperative languages.
In the vast majority of cases, in the Mercury compiler we use parameter structures.
We do use mutables in a small handful of cases, but

- almost all our mutables are attached to the I/O state,
  having all their get and set operations be pure code that
  takes a pair of I/O states,

- the one or two mutables that are accessed using semipure
  get operations that do NOT take a pair of I/O state arguments
  were designed that way only because passing either a params structure
  or a pair of I/O states through the code that needed access to the value
  in that mutable would have been possible only at the cost of making
  an already complicated piece of code even harder to read.
  Even then, the only reason why I even *considered* doing that
  was that our use case was scenario A, i.e. the value in the mutable
  was read-only after initialization. (And actually, as I looked at that
  code to answer your question, I am thinking of changing that code
  to use sort-of parameter structures after all.)

> however, I have experimented with a second version of load_font in my binding that does not use !IO:
> 
>    1162
>    1163 :- pragma foreign_proc(
>    1164     "C", load_font2(FontName::in, Out::out),
>    1165     [ promise_pure, will_not_call_mercury, will_not_throw_exception
>    1166     , will_not_modify_trail, thread_safe, does_not_affect_liveness
>    1167     ],
>    1168     "
>    1169         void* p = MR_GC_malloc_uncollectable(sizeof(Font));
>    1170         Out = p;
>    1171         Font font = LoadFont(FontName);
>    1172         memcpy(p, &font, sizeof(Font));
>    1173     ").

Whatever you do, do NOT choose this approach. It is tricky even for
Mercury system implementors to know whether this code would be
guaranteed to work; for anyone else, it can work only by accident.

> They both work, and it means that I can now load fonts by adding an initialise call to the module and then loading fonts without having an !IO context. I think I might change the mutable to be a `univ` on the grounds that after module initialisation, I know it will contain a fully populated `font_state`.

Univs are intended for situations in which you need to be able to handle
values of arbitrary types, with the set of types not knowable in advance.
In this case, you *know* what type the value you want will have,
you may just not have the value yet. To represent this, you definitely want
something like the datrie_status type from Mark's reply, and NOT univs.

> I envisaged the new call looking something like this:
> 
> :- mutable(fstate, font_state,
>        font_state(
>            load_default_font,
>            load_a_font("path/to/main/font.ttf")
>        ),
>        ground,
>        [untrailed]
> ).
> 
> ...but I would have to write a function shim as well
> 
> :- func load_a_font(string::in) = (rfont::out) is det.
> load_a_font(Path) = F :-
>     load_font(Path, F).  % No !IO context!

Sorry, but I have no idea what this means. In this code,
load_a_font and load_font are exactly equivalent modulo the fact
that one is a function and the other is a predicate, so that
clause accomplishes nothing. I have no idea what kind of "shim"
this is supposed to be. (If you are referring to the fact that the
initial value of a mutable may be constructed using a function
but not using a predicate, you still don't necessarily need a shim:
you can just *turn* the predicate you want to use into a function.)
 
> Sorry for rambling thus far, but my question is this: if I remove "!IO" from all of my binding to Raylib, what price might I pay in my code? The Raylib environment is hidden inside the library, any allocations it does are not known to Mercury, so what possible problems might I face if Mercury sees the same bunch of `det` calls but they don't need IO state in and out? Is that "lying" to Mercury in such a way as it could cause subtle issues, I am writing an IDE so it's a non-trivial project.

The answers to such questions depend almost entirely on your ultimate
purpose. Without a description of your purpose, the more detailed the better
(such as scenario A vs B vs some C), no one can give you a useful answer.

Zoltan.





More information about the users mailing list