Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImportC: Add a pragma for ignoring function declarations and definitions with specific identifiers. #16464

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

just-harry
Copy link
Contributor

@just-harry just-harry commented May 9, 2024

Spec PR: dlang/dlang.org#3828


This PR adds an ImportC-specific #pragma importc_ignore.
This pragma is used to ignore kinds of declarations and definitions with specific identifiers.
This is for situations where it is desirable for ImportC to look past a C declaration/definition of a symbol, and instead use a declaration/definition from an __imported D module.

A real world use-case for this would be handling MSVC compiler intrinsics when MSVC's #pragma intrinsic is used. (More on that later.)

I can also think of situations where it could be very convenient to be able to ignore a function-definition: such as to replace a C function's implementations with an __imported D function, if the C function happens to use compiler-extensions not supported by ImportC; or, even just to fix bugs, or add logging, or whatever, without having to bother with a whole fork.


#pragma importc_ignore has one form:

  1. #pragma importc_ignore(+/-<category>... : <identifier>,...)

That is, a sequence of plus-or-minus-prefixed categories, followed by a colon, and then a comma-separated list of identifiers.

The categories recognized by #pragma importc_ignore are as follows:

  • function_decl: which ignores function declarations, e.g. void foo(int);.
  • function_def: which ignores function definitions, e.g. void foo(int x) {}.

When a category is prefixed with a plus (+), declarations/definitions of that category will begin to be ignored if their identifier is included in the list of identifiers supplied to #pragma importc_ignore.
When a category is prefixed with a minus (-), declarations/definitions of that category will no longer be ignored if their identifier is included in the list of identifiers supplied to #pragma importc_ignore.

For example:

// We start ignoring function-declarations and function-definitions of `foo` and `bar`.
#pragma importc_ignore(+function_decl +function_def : foo, bar)
// This declaration of `foo` is ignored.
void foo(int);
// As is this definition of `foo`.
void foo(int x)
{}

// We stop ignoring function-definitions of `bar`.
#pragma importc_ignore(-function_def : bar)
// This declaration of `bar` is ignored.
void bar(int);
// This definition of `bar` is not ignored.
float bar(float x, float y)
{
    return x * y;
}

The rationale behind allowing declarations and definitions to be ignored separately is to make it easier to account for situations where a C library may conditionally declare or define a function based on preprocessor #defines.
E.g. the same function is declared when targeting one platform but defined when targeting a different platform.


Back to a real world use-case for this—MSVC has #pragma intrinsic, which causes the compiler to treat calls to identifiers supplied to #pragma intrinsic as calls to intrinsics, and not calls to functions.

For example, in the standard Windows headers _ReadWriteBarrier, and hundreds of other intrinsics, are defined as so:

void _ReadWriteBarrier(void);
#pragma intrinsic(_ReadWriteBarrier)

The presence of #pragma intrinsic(_ReadWriteBarrier) means that no actual calls to _ReadWriteBarrier are emitted, despite the body-less declaration of _ReadWriteBarrier.

Whereas, in ImportC, because #pragma intrinsic has no effect, the body-less declaration of _ReadWriteBarrier will cause actual calls to _ReadWriteBarrier to be emitted—and worse, the body-less declaration of _ReadWriteBarrier will prevent the compiler from using an __imported definition of _ReadWriteBarrier.

This worsens runtime performance, as D implementations of #pragma intrinsic intrinsics cannot be inlined, and increases compile-times as codegen for such D implementations must be performed unconditionally.

However, if we were to implement and use #pragma importc_ignore, then D implementations of #pragma intrinsic intrinsics could be inlined, and can be templates: improving runtime performance, and reducing compile-times as codegen need be performed only for the intrinsics that are actually used.

This would be achieved with a simple:

#pragma importc_ignore(+function_decl : _ReadWriteBarrier)
void _ReadWriteBarrier(void);
#pragma intrinsic(_ReadWriteBarrier)

As a side-note, this is my third iteration of this kind of pragma.

The first was to reimplement MSVC's #pragma intrinsic, and #pragma function, by means of forwarding #pragma intrinsic functions to a D module specified by #pragma intrinsicsModule.

The second was to refine that to just one #pragma forwardTo, which would rewrite function calls to a target, like so: #pragma forwardTo(set c_intrinsics : foo), which would rewrite foo() to c_intrinsics.foo().
But, the implementation for that ends up being more complex than the implementation for importc_ignore, and importc_ignore achieves more-or-less the same effect.
Outside of parsing the pragma and keeping track of what's to be ignored, the implementation for this is only two if-statements.

I did consider also allowing declarations/definitions of structs, unions, and enums, and typedefs to be ignored, but that then raises the question of whether they're ignored in all scopes or only the global scope. Whereas, functions have only global scope in C; so to keep things simple: functions only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant