Thread overview
Pass D const pointer to opaque C library: Guarantees? Optimization-safe?
Dec 16, 2017
SimonN
Dec 16, 2017
Mike Parker
Dec 16, 2017
Mike Parker
Dec 16, 2017
SimonN
December 16, 2017
Hi,

I'm calling a C library through a D wrapper. The situation is like this:

C library has:
    struct A      { ... };
    A* create_a() { ... }
    void foo(A*)  { ... }

D wrapper declares:
    extern (C) {
        struct A {}
        A* create_a();
        void foo(A*);
    }

My D usercode:
    const A* a = create_a();
    foo(cast(A*) a);

We know that const is transitive in D and the compiler may optimize around it. If we cast away const, it's our responsibility that, e.g., the optimized caching from const will not cause bugs.

The memory of the A is allocated by the C library. All the D code ever sees is a pointer, an opaque handle to the resource. How will the compiler optimizations behave around this:

Question 1. Is that cast still safe in usercode if foo(a) changes some internal values in *a that are undetectable through the C API?

Question 2. If yes, can the wrapper sanely declare foo(const A*) instead of foo(A*)?

My use case: My const-heavy D usercode calls Allegro 5, a C game/multimedia library without any const in its API, through the D bindings DAllegro5. I'm considering to make a PR implementing question 2. Github issue: https://github.com/SiegeLord/DAllegro5/issues/42

-- Simon
December 16, 2017
On Saturday, 16 December 2017 at 08:56:59 UTC, SimonN wrote:
> Hi,
>
> I'm calling a C library through a D wrapper. The situation is like this:
>
> C library has:
>     struct A      { ... };
>     A* create_a() { ... }
>     void foo(A*)  { ... }
>
> D wrapper declares:
>     extern (C) {
>         struct A {}
>         A* create_a();
>         void foo(A*);
>     }

Just a nitpick, because sometimes the distinction matters: that's a binding, not a wrapper.


>
> The memory of the A is allocated by the C library. All the D code ever sees is a pointer, an opaque handle to the resource. How will the compiler optimizations behave around this:
>
> Question 1. Is that cast still safe in usercode if foo(a) changes some internal values in *a that are undetectable through the C API?

Not sure what you mean by "safe" in this context, but the mandate of const in D is that data will not change through that particular reference. It says nothing about what happens through other references to the same data, whether they be in D or C. If it's important to you that no data in an instance of A changes, then you'll have to audit the C functions to make sure they aren't changing anything. If it's not important, i.e. you only want to prevent changes on the D side and don't care if they happen on the C side, then that's fine.

>
> Question 2. If yes, can the wrapper sanely declare foo(const A*) instead of foo(A*)?

In D, const function parameters serve as a bridge between mutable and immutable variables -- this particular implementation would accept A*, const A*, and immutable A*. For most purposes, on a C function binding it's nothing more than an annotation. Where it matters is if you pass immutable variables to the function -- if the parameter is const even when the function modifies the variable, D will allow immutable to be passed and you're looking at unexpected behavior.

So I would say it's not a good idea in the general case. Only add const to parameters in C function declarations if the C API actually declares those parameters as const.

>
> My use case: My const-heavy D usercode calls Allegro 5, a C game/multimedia library without any const in its API, through the D bindings DAllegro5. I'm considering to make a PR implementing question 2. Github issue: https://github.com/SiegeLord/DAllegro5/issues/42

I would expect SiegeLord to reject such a PR. I know I would if someone submitted one to any Derelict packages. I suggest making wrapper functions for your specific use case (hence my insistence on the distinction above), preferably combining mutliple function calls into one where it makes sense. Since you know if you're using immutable variables or not and when it's fine to cast away const, you can make the parameters to the wrapper functions const and cast it  away inside.



December 16, 2017
On Saturday, 16 December 2017 at 11:19:36 UTC, Mike Parker wrote:

> I would expect SiegeLord to reject such a PR.

And now that I clicked through the link, I see I was wrong :-) In principle, I disagree with him because of what I mentioned above about immutable variables. In practice, it's probably never going to be an issue. The question is how much weight should be assigned to "probably".
December 16, 2017
On Saturday, 16 December 2017 at 11:19:36 UTC, Mike Parker wrote:
> that's a binding, not a wrapper.

Right!

> Not sure what you mean by "safe"
> you only want to prevent changes on the D side and don't care
> if they happen on the C side, then that's fine.

This, yes. I'd like const-annotated D code because of the static checks, without risking bugs from aggressive compiler assumptions. It's fine it the C side mutates privately.

I am ready to take responsibility in case I misjudge whether one of the C functions mutates  detectable state or not.

>> Question 2. If yes, can the wrapper sanely declare foo(const A*) instead of foo(A*)?
> if you pass immutable variables to the function -- if the
> parameter is const even when the function modifies the
> variable, D will allow immutable to be passed and you're
> looking at unexpected behavior.

This is the heart of the problem, I've overlooked this.

None of my A are immutable, but declaring the bindings as foo(const A*) would take immutables.

> I would expect SiegeLord to reject such a PR.
> Only add const to parameters in C function declarations if
> the C API actually declares those parameters as const.

That was my hunch, too. I've asked upstream on the Allegro forums. It would be a drastic change, I'd wager there won't be any const in the API anytime soon. But I'll give them the due time to decide.

If no const C API, I'd stick to private wrappers around DAllegro5, with a codebase-wide rule to not pass immutable.

> In principle, I disagree with him
> how much weight should be assigned to "probably".

Hmm, SiegeLord is a core Allegro 5 developer, he could judge overriding the C API's promises. But I share your sentiment that the public C API should have the final word.

Many thanks for the high-quality answer!

-- Simon