Search code examples
annotationssal

Can Sal annotate that parameter members may be mutated?


I am writing a reference-counted linked list of characters data structure in C for practice. I want to try using Sal in it to annotate function parameters for this practice.

I have an input paremeter(named This), which I want to annotate to make it clear that the specified parameter's members must be mutable in order for the function to behave as expected.

The situation is analogous to the code below.

#include <Windows.h>

typedef struct Box {
    ULONG val;
} Box;

ULONG Box_decrement(_In_ Box *This) {
    return InterlockedDecrement(&(This->val));
}

int main(int argc, char **argv) {
    Box b = {2};
    Box_decrement(&b);

    return (BYTE)b.val;
};

Is there an existing Sal annotation that can be used to annotate the This parameter of the Box_increment function to make it clear from the function signature that the function modifies one or more members of the Box that has been passed to it?

Something like _InternallyMutable_(but exist):

#include <Windows.h>

typedef struct Box {
    ULONG val;
} Box;

ULONG Box_decrement(_InternallyMutable_ _In_ Box *This) {
    return InterlockedDecrement(&(This->val));
}

int main(int argc, char **argv) {
    Box b = {2};
    Box_decrement(&b);

    return (BYTE)b.val;
};

Best solution so far(unfortunately, there does not seem to be any equivelent in SAL to denote Internally_mutable, there is Unchanged which is the opposite):

#include <Windows.h>

#define _Internally_mutable_(expr) _At_(expr, _Out_range_(!=, _Old_(expr)))

typedef struct Box {
    ULONG val;
} Box;

ULONG Box_decrement(_In_ _InternallyMutable_(This) Box *This) {
    return InterlockedDecrement(&(This->val));
}

int main(int argc, char **argv) {
    Box b = {2};
    Box_decrement(&b);

    return (BYTE)b.val;
};

Solution

  • Yes! You can. SAL is a wonderful DSL that lets you do basically anything you want if you're psychic enough to infer it from the little bits in the Windows SDK. I've even in the past been able to write super simple custom annotations to detect invalid HANDLE usage with _Post_satisfies_ and friends.

    This code seems to work:

    _At_(value, _Out_range_(!=, _Old_(value)))
    void change_value_supposed_to(int& value) noexcept {
        //value += 1;
    }
    

    ...Running with all native rules in code analysis, I get a warning like this:

    Warning C28196  The requirement that '_Param_(1)!=(("pre"), _Param_(1))' is not satisfied. (The expression does not evaluate to true.)
    

    (there, substitute value with your variable) For _Internally_mutable_, I can do it in the "above the function" style of SAL:

    #define _Internally_mutable_(expr) _At_(expr, _Out_range_(!=, _Old_(expr)))
    _Internally_mutable_(value)
    void change_value_supposed_to_internally_mutable(int& value) noexcept {
        //value += 1;
        (void)value;
    }
    

    ...but not inline WITHOUT being repetitive, as you wanted. Not sure why right now - _Curr_ doesn't seem to be working? - I may need another layer of indirection or something. Here's what it looks like:

    #define _Internally_mutable_inline_(value) _Out_range_(!=, _Old_(value))
    void change_value_supposed_to_internally_mutable_inline(_Internally_mutable_inline_(value) int& value) noexcept {
        //value += 1;
        (void)value;
    }
    

    How I figured this out:

    sal.h defines an _Unchanged_ annotation (despite doing web dev for several years now and little C++, I remembered this when I saw your question in a google alert for SAL!):

    // annotation to express that a value (usually a field of a mutable class)
    // is not changed by a function call
    #define _Unchanged_(e)              _SAL2_Source_(_Unchanged_, (e), _At_(e, _Post_equal_to_(_Old_(e)) _Const_))
    

    ...if you look at this macro closely, you'll see that it just substitutes as:

    _At_(e, _Post_equal_to_(_Old_(e)) _Const_)
    

    ...and further unrolling it, you'll see _Post_equal_to_ is:

    #define _Post_equal_to_(expr)       _SAL2_Source_(_Post_equal_to_, (expr), _Out_range_(==, expr))
    

    Do you see it? All it's doing is saying the _Out_range_ is equal to the expression you specify. _Out_range_ (and all the other range SAL macros) appear to accept all of the standard C operators. That behavior is not documented, but years of reading through the Windows SDK headers shows me it's intentional! Here, all we need to do is use the not equals operator with the _Old_ intrinsic, and the analyzer's solver should be able to figure it out!

    _Unchanged_ itself is broken?

    To my great confusion, _Unchanged_ itself seems broken:

    _Unchanged_(value)
    void change_value_not_supposed_to(_Inout_ int& value) noexcept {
        value += 1;
    }
    

    ...that produces NO warning. Without the _Inout_, code analysis is convinced that value is uninitialized on function entry. This makes no sense of course, and I'm calling this directly from main in the same file. Twiddling with inlining or link time code generation doesn't seem to help

    I've played a lot with it, and various combinations of _Inout_, even _Post_satisfies_. I should file a bug, but I'm already distracted here, I'm supposed to be doing something else right now :)

    Link back here if anybody does file a bug. I don't even know what the MSVC/Compiler teams use for bug reporting these days.

    Fun facts

    5-6 years ago I tried to convince Microsoft to open source the SAL patents! It would have been great, I would have implemented them in Clang, so we'd all be able to use it across platforms! I might have even kicked off a career in static-analysis with it. But alas, they didn't want to do it in the end. Open sourcing them would have meant they might have to support it and/or any extensions the community might have introduced, and I kinda understand why they didn't want that. It's a shame, I love SAL, and so do many others!