Skip to content

Calling Convention Lowering: support function pointers #995

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

Closed
gitoleg opened this issue Oct 18, 2024 · 5 comments
Closed

Calling Convention Lowering: support function pointers #995

gitoleg opened this issue Oct 18, 2024 · 5 comments

Comments

@gitoleg
Copy link
Collaborator

gitoleg commented Oct 18, 2024

Hi all.
I faced with the necessity of function pointer support in the calling convention lowering pass. And here are several examples that demonstrate the complexity of the problem. I don't even touch calls to functions at all - it's not a problem so far.

The next types are used in the examples below:

typedef struct {
  int a;
} S; 

typedef int (*myfptr)(S); 

typedef struct {
  myfptr f;
} T; 

Example 1

int foo(S s) { return s.a; }

void bar() {
    S s;
    myfptr f = foo;
    f(s);
}

%1 = cir.alloca !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>, ["a", init] {alignment = 8 : i64}
%2 = cir.get_global @foo : !cir.ptr<!cir.func<!s32i (!ty_S)>>
cir.store %2, %1 : !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>> 

As one can see, the code above needed to be rewritten in order to comply with a calling convetion.

First of all, this code won't compile at all:

'cir.get_global' op result type pointee type ''!cir.func<!cir.int<s, 32> (!cir.struct<struct "S" {!cir.int<s, 32>} #cir.record.decl.ast>)>'' does not match type '!cir.func<!cir.int<s, 32> (!cir.int<s, 32>)>' of the global @foo

Since @foo has the proper type already: '!cir.func<!cir.int<s, 32> (!cir.int<s, 32>)>' Ok. we can fix this - and rewrite GetGlobalOp.
But next we have to do something with AllocaOp - we can either perform a cast of the get_global result or rewrite the alloca operation. The former is easier but is not applicable for all the cases (see the next example). The latter means we need to replace AllocaOp and update its users.

Also, we could relax the GetGlobalOp verification - but it's not a good way at all and anyway we still depend on the corresponded operation verification from LLVM dialect, which may change eventually.

Example 2

void bar() {
  myfptr a[2] = {foo, foo};
}
%1 = cir.alloca !cir.array<!cir.ptr<!cir.func<!s32i (!ty_S)>> x 2>, !cir.ptr<!cir.array<!cir.ptr<!cir.func<!s32i (!ty_S)>> x 2>>, ["a"] {alignment = 16 : i64}
%2 = cir.const #cir.const_array<[#cir.global_view<@foo> : !cir.ptr<!cir.func<!s32i (!ty_S)>>, #cir.global_view<@foo> : !cir.ptr<!cir.func<!s32i (!ty_S)>>]> : !cir.array<!cir.ptr<!cir.func<!s32i (!ty_S)>> x 2>
cir.store %2, %1 : !cir.array<!cir.ptr<!cir.func<!s32i (!ty_S)>> x 2>, !cir.ptr<!cir.array<!cir.ptr<!cir.func<!s32i (!ty_S)>> x 2>>

Here we have the const array of function pointers (not lowered ones, i.e. of a wrong type) and ConstantOp which (I believe) we can not simple cast to another type, so StoreOp is wrong. So we have to rewrite it and again - rewrite AllocaOp and it's users.

Example 3

void bar() {
  T t;
  t.f = foo;
}
%2 = cir.get_global @foo : !cir.ptr<!cir.func<!s32i (!ty_S)>> 
%3 = cir.get_member %1[0] {name = "f"} : !cir.ptr<!ty_T> -> !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>
cir.store %2, %3 : !cir.ptr<!cir.func<!s32i (!ty_S)>>

Here the struct type is involved as a storage for the function pointer. Again either cast is needed or we need to change the struct type itself.

Solution ?

So what I eager to find a good approach for the problem. Right now I think that the existing pass should match against many operations:
AllocaOp, LoadOp, StoreOp, GetMemberOp, GetGlobalOP ... - looks like its not enough just to call replaceAllUsesWith once the value type is changed. Also, the pass should perform type conversions (Example 3). I m not sure that it's a good way though - so may be you think of something else ? And the solution is simplier?
@bcardosolopes @sitio-couto

@smeenai
Copy link
Collaborator

smeenai commented Oct 18, 2024

CC @bruteforceboy, who's been looking at this recently too (e.g. #979)

@sitio-couto
Copy link
Collaborator

sitio-couto commented Oct 18, 2024

Hey @gitoleg !

First of all, this code won't compile at all

Function pointers are not yet handled by CC lowering. There should be an assertion noting this but it's not covering all cases.

So what I eager to find a good approach for the problem.

Function pointers are also tricky because CIR pointers are not opaque, so any related allocas will have the original function type instead of the ABI-specific one. I was looking into this since pointers are the major missing thing in CC lowering, but didn't have the time to deep dive in it.

My main idea was:

  • Rewrite the function type with its ABI-specific counterpart.
  • Replace any references to it (through the symbol interface).
  • Bit cast any function pointer handling to and from the ABI-agnostic function type as necessary.

The last item is the trick as it may prevent us from rewriting a bunch of stuff that uses the ABI-agnostic function type (though I'm not sure if it would cover all of the cases you listed). After rewriting get_global with the ABI-specific function type, bit cast it to the ABI-agnostic one to keep the ball rolling. These pointer-to-pointer bitcasts are also dropped during lowering, so they wouldn't affect the resulting LLVM code.

Another approach would be to support opaque pointers in CIR. We keep pointee types to preserve high-level info, but considering that CC lowering is applied before moving to LLVM Dialect, and both LLVM Dialect and IR only use opaque pointers, maybe this is the way to go.

@bcardosolopes
Copy link
Member

Another approach would be to support opaque pointers in CIR ... Bit cast any function pointer handling to and from the ABI-agnostic function type as necessary.

We've found LLVM ARM intrinsics that still require pointer info to be lowered correctly with elementtype (and the LLVM IR dialect doesn't yet support, but this is just a fixable details), I'd prefer not to run into these issues in the future because we lost information. Perhaps we could reconsider once CIR is more mature and has full coverage of the things still remaining not opaque in LLVM IR.

@bcardosolopes
Copy link
Member

So what I eager to find a good approach for the problem. Right now I think that the existing pass should match against many operations: AllocaOp, LoadOp, StoreOp, GetMemberOp, GetGlobalOP ...

Should we create an interface that all operations that need ABI fixing can use and be tracked by?

ConstantOp which (I believe) we can not simple cast to another type

Assuming it only has one use, for ConstantOp shouldn't it be fine to just change it's type?

bcardosolopes pushed a commit that referenced this issue Oct 30, 2024
…tion lowering pass (#1003)

This PR adds initial function pointers support for the calling
convention lowering pass. This is a suggestion, so any other ideas are
welcome.

Several ideas was described in the #995 and basically what I'm trying to
do is to generate a clean CIR code without additional `bitcast`
operations for function pointers and without mix of lowered and initial
function types.

#### Problem
Looks like we can not just lower the function type and cast the value
since too many operations are involved. For instance, for the
next simple code: 
```
typedef struct {
  int a;
} S;

typedef int (*myfptr)(S);

int foo(S s) { return 42 + s.a; }

void bar() {
  myfptr a = foo;
}
```
we get the next CIR for the function `bar` , before the calling
convention lowering pass:
```
cir.func no_proto  @bar() extra(#fn_attr) {
    %0 = cir.alloca !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>, ["a", init]
    %1 = cir.get_global @foo : !cir.ptr<!cir.func<!s32i (!ty_S)>> 
    cir.store %1, %0 : !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>
    cir.return 
  } 
```
As one can see, first three operations depend on the function type. Once
`foo` is lowered, we need to fix `GetGlobalOp`:
otherwise the code will fail with the verification error since actual
`foo` type (lowered) differs from the one currently expected by the
`GetGlobalOp`.
First idea would just rewrite only the `GetGlobalOp` and insert a
bitcast after, so both `AllocaOp` and `StoreOp` would work witth proper
types.

Once the code will be more complex, we will need to take care about
possible use cases, e.g. if we use arrays, we will need to track array
accesses to it as well in order to insert this bitcast every time the
array element is needed.

One workaround I can think of: we fix the `GetGlobalOp` type and cast
from the lowered type to the initial, and cast back before the actual
call happens - but it doesn't sound as a good and clean approach (from
my point of view, of course).

So I suggest to use type converter and rewrite any operation that may
deal with function pointers and make sure it has a proper type, and we
don't have any unlowered function type in the program after the calling
convention lowering pass.

#### Implementation
I added lowering for `AllocaOp`, `GetGlobalOp`, and split the lowering
for `FuncOp` (former `CallConvLoweringPattern`) and lower `CallOp`
separately.
Frankly speaking, I tried to implement a pattern for each operation, but
for some reasons the tests are not passed for
windows and macOs in this case - something weird happens inside
`applyPatternsAndFold` function. I suspect it's due to two different
rewriters used - one in the `LoweringModule` and one in the mentioned
function.
So I decided to follow the same approach as it's done for the
`LoweringPrepare` pass and don't involve this complex rewriting
framework.

Next I will add a type converter for the struct type, patterns for
`ConstantOp` (for const arrays and `GlobalViewAttr`)
In the end of the day we'll have (at least I hope so) a clean CIR code
without any bitcasts for function pointers.

cc @sitio-couto  @bcardosolopes
@gitoleg
Copy link
Collaborator Author

gitoleg commented Nov 1, 2024

I thing we can close this so far, implemented in #1003 and in #1034

@gitoleg gitoleg closed this as completed Nov 1, 2024
lanza pushed a commit that referenced this issue Nov 5, 2024
…tion lowering pass (#1003)

This PR adds initial function pointers support for the calling
convention lowering pass. This is a suggestion, so any other ideas are
welcome.

Several ideas was described in the #995 and basically what I'm trying to
do is to generate a clean CIR code without additional `bitcast`
operations for function pointers and without mix of lowered and initial
function types.

#### Problem
Looks like we can not just lower the function type and cast the value
since too many operations are involved. For instance, for the
next simple code: 
```
typedef struct {
  int a;
} S;

typedef int (*myfptr)(S);

int foo(S s) { return 42 + s.a; }

void bar() {
  myfptr a = foo;
}
```
we get the next CIR for the function `bar` , before the calling
convention lowering pass:
```
cir.func no_proto  @bar() extra(#fn_attr) {
    %0 = cir.alloca !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>, ["a", init]
    %1 = cir.get_global @foo : !cir.ptr<!cir.func<!s32i (!ty_S)>> 
    cir.store %1, %0 : !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>
    cir.return 
  } 
```
As one can see, first three operations depend on the function type. Once
`foo` is lowered, we need to fix `GetGlobalOp`:
otherwise the code will fail with the verification error since actual
`foo` type (lowered) differs from the one currently expected by the
`GetGlobalOp`.
First idea would just rewrite only the `GetGlobalOp` and insert a
bitcast after, so both `AllocaOp` and `StoreOp` would work witth proper
types.

Once the code will be more complex, we will need to take care about
possible use cases, e.g. if we use arrays, we will need to track array
accesses to it as well in order to insert this bitcast every time the
array element is needed.

One workaround I can think of: we fix the `GetGlobalOp` type and cast
from the lowered type to the initial, and cast back before the actual
call happens - but it doesn't sound as a good and clean approach (from
my point of view, of course).

So I suggest to use type converter and rewrite any operation that may
deal with function pointers and make sure it has a proper type, and we
don't have any unlowered function type in the program after the calling
convention lowering pass.

#### Implementation
I added lowering for `AllocaOp`, `GetGlobalOp`, and split the lowering
for `FuncOp` (former `CallConvLoweringPattern`) and lower `CallOp`
separately.
Frankly speaking, I tried to implement a pattern for each operation, but
for some reasons the tests are not passed for
windows and macOs in this case - something weird happens inside
`applyPatternsAndFold` function. I suspect it's due to two different
rewriters used - one in the `LoweringModule` and one in the mentioned
function.
So I decided to follow the same approach as it's done for the
`LoweringPrepare` pass and don't involve this complex rewriting
framework.

Next I will add a type converter for the struct type, patterns for
`ConstantOp` (for const arrays and `GlobalViewAttr`)
In the end of the day we'll have (at least I hope so) a clean CIR code
without any bitcasts for function pointers.

cc @sitio-couto  @bcardosolopes
lanza pushed a commit that referenced this issue Mar 18, 2025
…tion lowering pass (#1003)

This PR adds initial function pointers support for the calling
convention lowering pass. This is a suggestion, so any other ideas are
welcome.

Several ideas was described in the #995 and basically what I'm trying to
do is to generate a clean CIR code without additional `bitcast`
operations for function pointers and without mix of lowered and initial
function types.

Looks like we can not just lower the function type and cast the value
since too many operations are involved. For instance, for the
next simple code:
```
typedef struct {
  int a;
} S;

typedef int (*myfptr)(S);

int foo(S s) { return 42 + s.a; }

void bar() {
  myfptr a = foo;
}
```
we get the next CIR for the function `bar` , before the calling
convention lowering pass:
```
cir.func no_proto  @bar() extra(#fn_attr) {
    %0 = cir.alloca !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>, ["a", init]
    %1 = cir.get_global @foo : !cir.ptr<!cir.func<!s32i (!ty_S)>>
    cir.store %1, %0 : !cir.ptr<!cir.func<!s32i (!ty_S)>>, !cir.ptr<!cir.ptr<!cir.func<!s32i (!ty_S)>>>
    cir.return
  }
```
As one can see, first three operations depend on the function type. Once
`foo` is lowered, we need to fix `GetGlobalOp`:
otherwise the code will fail with the verification error since actual
`foo` type (lowered) differs from the one currently expected by the
`GetGlobalOp`.
First idea would just rewrite only the `GetGlobalOp` and insert a
bitcast after, so both `AllocaOp` and `StoreOp` would work witth proper
types.

Once the code will be more complex, we will need to take care about
possible use cases, e.g. if we use arrays, we will need to track array
accesses to it as well in order to insert this bitcast every time the
array element is needed.

One workaround I can think of: we fix the `GetGlobalOp` type and cast
from the lowered type to the initial, and cast back before the actual
call happens - but it doesn't sound as a good and clean approach (from
my point of view, of course).

So I suggest to use type converter and rewrite any operation that may
deal with function pointers and make sure it has a proper type, and we
don't have any unlowered function type in the program after the calling
convention lowering pass.

I added lowering for `AllocaOp`, `GetGlobalOp`, and split the lowering
for `FuncOp` (former `CallConvLoweringPattern`) and lower `CallOp`
separately.
Frankly speaking, I tried to implement a pattern for each operation, but
for some reasons the tests are not passed for
windows and macOs in this case - something weird happens inside
`applyPatternsAndFold` function. I suspect it's due to two different
rewriters used - one in the `LoweringModule` and one in the mentioned
function.
So I decided to follow the same approach as it's done for the
`LoweringPrepare` pass and don't involve this complex rewriting
framework.

Next I will add a type converter for the struct type, patterns for
`ConstantOp` (for const arrays and `GlobalViewAttr`)
In the end of the day we'll have (at least I hope so) a clean CIR code
without any bitcasts for function pointers.

cc @sitio-couto  @bcardosolopes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants