c - clang python Bindings: how to get the signature of the function pointer from the CALL_EXPR cursor - Stack Overflow

I need to find the signatures of each function pointer calling expression in a ".c" source fi

I need to find the signatures of each function pointer calling expression in a ".c" source file, using clang API in Python.

I don't know if clang has a way to tell whether a CALL_EXPR is a function pointer call or a normal function call, so far this is the only workaround I have.

c: Cursor
if c.kind == CursorKind.CALL_EXPR:
    if not (c.referenced and c.referenced.kind == CursorKind.FUNCTION_DECL):
        # funtion pointer
        # ...

Another troublesome problem is that I want to know the function signature that this function pointer represents, but clang doesn't seem to provide a simple interface for getting the type of the return value. But fortunately, it provides an argument list for the function pointer.

# the return type is invalid
result_type: Type = c.result_type

# the argument list is correct
arg_list = str(', ').join([arg.type.get_canonical().spelling for arg in c.get_arguments()])
print(f'{arg_list}')

I don't want to delve into this CALL_EXPR, because the the expression that returns this function pointer can be very complex, can be a simple function pointer variable, or a type cast, or even a ?: expression.

I just want to know if there's an easy way to get the specific signature of this function pointer.

I need to find the signatures of each function pointer calling expression in a ".c" source file, using clang API in Python.

I don't know if clang has a way to tell whether a CALL_EXPR is a function pointer call or a normal function call, so far this is the only workaround I have.

c: Cursor
if c.kind == CursorKind.CALL_EXPR:
    if not (c.referenced and c.referenced.kind == CursorKind.FUNCTION_DECL):
        # funtion pointer
        # ...

Another troublesome problem is that I want to know the function signature that this function pointer represents, but clang doesn't seem to provide a simple interface for getting the type of the return value. But fortunately, it provides an argument list for the function pointer.

# the return type is invalid
result_type: Type = c.result_type

# the argument list is correct
arg_list = str(', ').join([arg.type.get_canonical().spelling for arg in c.get_arguments()])
print(f'{arg_list}')

I don't want to delve into this CALL_EXPR, because the the expression that returns this function pointer can be very complex, can be a simple function pointer variable, or a type cast, or even a ?: expression.

I just want to know if there's an easy way to get the specific signature of this function pointer.

Share Improve this question asked Nov 20, 2024 at 13:01 VerySimpleVerySimple 1846 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

Recognizing indirect function calls

When using the Python libclang bindings, given a CALL_EXPR node, to check whether it is a direct function call:

  • Get the callee node as the first child of the call node.

  • Skip implicit casts and parentheses.

  • Then check if it is a DECL_REF_EXPR to a FUNCTION_DECL. If so, it is direct.

This is similar to what the code in the question does, except that uses the referenced attribute of the call node, which (as it happens) does not work if the function call is direct but the name of the callee is inside parentheses (which is sometimes done to avoid invocation of a same-named macro).

Getting the type signature of the callee

To get the type signature of the callee:

  • Again, get the callee node as the first child of the call node, and skip implicit casts and parentheses.

  • Get its type as callee.type.

  • If it is a POINTER type (which happens when a function pointer is not explicitly dereferenced), skip to the pointee.

  • If it is a FUNCTIONPROTO, use get_result() and argument_types() to get the return and parameter types.

  • Otherwise it should be FUNCTIONNOPROTO, which only has a return type.

The code in the question uses c.get_arguments(), but that retrieves the argument expressions. Those typically will be converted (implicitly or explicitly) to the type of the corresponding parameter if the function has a prototype, but I think it's better to get the type of the callee directly so as not to rely on the somewhat complicated mechanism of implicit argument conversion.

The code in the question also tries to use c.result_type to get the return type, but that is wrong since result_type is actually used to get the return type of a declaration to which a cursor might refer (this is not obvious; I had to look at the source code). To get the type of the call expression node, just use c.type. I think it's preferable to get the return type of the callee function type, but it should be the same as the type of the call node.

Complete example

Here is a Python script that uses the above methods to print some details about call expressions:

#!/usr/bin/env python3
"""Print function pointer signatures."""

import sys

from clang.cindex import Config, CursorKind, Index, TypeKind


def cursor_loc(c):
  """Return the location of `c` as a string."""

  return f"{c.location.line}:{c.location.column}"


def cursor_kind_and_loc(c):
  """Return the kind and location of `c` as a string."""

  return f"{c.kind} at {cursor_loc(c)}"


def get_first_child(c):
  """Return the first child of `c`, throwing if it does not have one."""

  children = c.get_children()
  for child in children:
    return child

  raise RuntimeError(
    f"get_first_child: expected {cursor_kind_and_loc(c)} to have a child")


def is_cast_or_paren(c):
  """True if `c` is a cast or parenthesis expression."""

  # Implicit casts unfortunately are "unexposed" in Python libclang.
  # That is one of its many deficiencies in comparison to the C++ Clang
  # API.
  return (c.kind == CursorKind.UNEXPOSED_EXPR or
          c.kind == CursorKind.PAREN_EXPR)


def skip_casts_and_parens(c):
  """If cursor `c` is a cast or parenthesis expression, return its
  sub-expression, recursively skipping any nested casts/parens."""

  if is_cast_or_paren(c):
    return skip_casts_and_parens(get_first_child(c))
  else:
    return c


def print_call_details(c):
  """Print details about function calls within the subtree rooted at
  cursor `c`."""

  if c.kind == CursorKind.CALL_EXPR:
    print(f"Call at {cursor_loc(c)}:")

    print(f"  call node type: {repr(c.type.spelling)}")

    calleeExpr = skip_casts_and_parens(get_first_child(c))
    if calleeExpr.kind == CursorKind.DECL_REF_EXPR:
      calleeDecl = calleeExpr.referenced

      # Call is direct if the callee declaration is a function.
      if calleeDecl.kind == CursorKind.FUNCTION_DECL:
        print(f"  direct callee: {repr(calleeDecl.spelling)}")

    calleeType = calleeExpr.type
    print(f"  callee type: {repr(calleeType.spelling)}")
    print(f"  callee type kind: {calleeType.kind}")

    if calleeType.kind == TypeKind.POINTER:
      # If the callee expression in an indirect call is not explicitly
      # dereferenced, we get a pointer type here that must be skipped.
      print(f"  skipping fn ptr type")
      calleeType = calleeType.get_pointee()

    if calleeType.kind == TypeKind.FUNCTIONPROTO:
      print(f"  return type: {repr(calleeType.get_result().spelling)}")

      i = 0
      for paramType in calleeType.argument_types():
        print(f"  parameter {i} type: {repr(paramType.spelling)}")
        i += 1

    elif calleeType.kind == TypeKind.FUNCTIONNOPROTO:
      print(f"  return type: {repr(calleeType.get_result().spelling)}")

      print(f"  no prototype, so no parameter info")

    else:
      print(f"  unexpected callee type kind: {calleeType.kind}")

  for child in c.get_children():
    print_call_details(child)


def main():
  # Load the Clang module.  On my Windows system, using Cygwin Python, I
  # seem to have to tell it explicitly the name of the DLL (it being on
  # the PATH is not enough).
  Config.set_library_file("/cygdrive/d/opt/winlibs-mingw64-13.2/bin/libclang.dll");
  index = Index.create()

  # Parse the C source code.  Disable the warning about functions
  # without prototypes just so we can exercise that case.
  tu = index.parse("test.c", args=["-Wno-deprecated-non-prototype"]);

  # Stop if there were syntax errors.
  if len(tu.diagnostics) > 0:
    for d in tu.diagnostics:
      print(d)
    sys.exit(2)

  # Parse was successful.  Inspect the AST.
  print_call_details(tu.cursor)


main()


# EOF

When run on the input:

// test.c
// Input for fnptr-sig.

int (*fnptr)(int, float);
int normal_function(int, float);

int (*fnptr_no_proto)();

void f(int a, float b)
{
  normal_function(a, b);
  (normal_function)(a, b);
  (*fnptr)(a, b);
  fnptr(a, b);
  fnptr_no_proto(a, b);
}

// EOF

it produces the output:

Call at 11:3:
  call node type: 'int'
  direct callee: 'normal_function'
  callee type: 'int (int, float)'
  callee type kind: TypeKind.FUNCTIONPROTO
  return type: 'int'
  parameter 0 type: 'int'
  parameter 1 type: 'float'
Call at 12:3:
  call node type: 'int'
  direct callee: 'normal_function'
  callee type: 'int (int, float)'
  callee type kind: TypeKind.FUNCTIONPROTO
  return type: 'int'
  parameter 0 type: 'int'
  parameter 1 type: 'float'
Call at 13:3:
  call node type: 'int'
  callee type: 'int (int, float)'
  callee type kind: TypeKind.FUNCTIONPROTO
  return type: 'int'
  parameter 0 type: 'int'
  parameter 1 type: 'float'
Call at 14:3:
  call node type: 'int'
  callee type: 'int (*)(int, float)'
  callee type kind: TypeKind.POINTER
  skipping fn ptr type
  return type: 'int'
  parameter 0 type: 'int'
  parameter 1 type: 'float'
Call at 15:3:
  call node type: 'int'
  callee type: 'int (*)()'
  callee type kind: TypeKind.POINTER
  skipping fn ptr type
  return type: 'int'
  no prototype, so no parameter info

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1742355041a4428261.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信