Function Pointer Dispatch
Many codebases use indirect function calls through struct fields (C) or interface methods (Go). Askl models these dispatch points as field symbols, enabling queries that trace from a caller through a dispatch point to the implementing function.
The Problem
In C, a common pattern is the vtable struct:
struct file_operations {
int (*read)(struct file *, char *, size_t);
int (*write)(struct file *, const char *, size_t);
};A function calls through the struct field:
ssize_t vfs_read(struct file *f, ...) {
f->f_op->read(f, buf, count); // indirect call
}And somewhere else, a driver provides the implementation:
static struct file_operations my_ops = {
.read = my_driver_read,
.write = my_driver_write,
};Without dispatch modeling, the indexer sees vfs_read calling a field access but has no way to connect it to my_driver_read. The field symbol type bridges this gap.
How It Works
The indexer creates three kinds of records for each dispatch point:
- Field symbol + instance at the struct field declaration (
file_operations.readin the header) - Call-site ref from each
fops->read(...)expression to thefile_operations.readfield symbol - Synthetic ref from the field’s declaration site to each implementing function (one per
.read = implassignment)
This creates a two-hop chain: caller → field → implementation.
Reference Chain
Given this source layout:
// fs.h
struct file_operations {
int (*read)(...); // field instance here
};
// drivers/my.c
static int my_read(...) { ... }
static struct file_operations ops = {
.read = my_read, // assignment site
};
// fs/read_write.c
ssize_t vfs_read(...) {
f->f_op->read(...); // call site
}The indexer produces:
| Ref | To symbol | From file | Attributed to |
|---|---|---|---|
| Call-site | file_operations.read | read_write.c | vfs_read function |
| Assignment | my_read | drivers/my.c | enclosing function/data |
| Synthetic | my_read | fs.h | file_operations.read field |
The synthetic ref is placed at the field’s declaration range in the header. Since the field’s instance covers that same range, the ref is attributed to the field by offset containment.
Querying Dispatch Chains
Find implementations of a field
field("file_operations.read") { func }Returns all functions assigned to the read field of file_operations. For Linux, this could return hundreds of driver implementations.
Broad match across structs
field("read") { func }Since "read" is a simple name (no separators), it matches any field whose last component is read across all structs (file_operations.read, address_space_operations.read, etc.).
Full dispatch chain from a caller
func("vfs_read") { field("read") { func } }Traces: vfs_read → calls through file_operations.read → implementing functions.
All fields of a struct (containment)
type("file_operations") has { field }Returns all function pointer fields declared in file_operations. Works because field (level 1) is below type (level 2) in the hierarchy.
Who calls through a field? (reverse)
{ field("file_operations.read") }Returns functions that reference file_operations.read — i.e., functions that call through the read dispatch point.
Combine with other verbs
preamble ignore("test") ignore("mock")
func("vfs_read") {
field("read") {
func { func }
}
}From vfs_read, through the read dispatch, find implementations and their callees — excluding test and mock code.
Go Interface Methods
In Go, interface methods serve the same role as C function pointer fields. The indexer models them identically using field symbols.
How It Works
For an interface declaration:
type Reader interface {
Read(p []byte) (n int, err error)
}The indexer creates a field symbol (io.Reader).Read with an instance at the method signature in the interface definition.
When a concrete type is assigned to an interface:
var r io.Reader = &MyFile{}The indexer creates refs from the interface method’s declaration to each matching concrete method (e.g., (*MyFile).Read).
Querying Go interfaces
The method verb is an alias for field, provided for readability in Go/OOP contexts:
method("Read") { func }Returns all concrete methods that implement a Read interface method.
type("Reader") has { method }Returns all method signatures declared in the Reader interface.
func("processData") { method("Read") { func } }Traces: processData calls through the Read interface method → concrete implementations.
Naming Conventions
| Language | Field name format | Example |
|---|---|---|
| C | struct_name.field_name | file_operations.read |
| Go | (pkg.Interface).Method | (io.Reader).Read |
Both use dots as separators, so matching works uniformly:
field("read")ormethod("Read")— simple name, matches the last component (leaf match)field("file_operations.read")ormethod("io.Reader.Read")— compound name, matches precisely (pattern match)
Limitations
| Case | Status |
|---|---|
Direct assignment (.read = impl) | Supported |
Binary assignment (ops->read = impl) | Supported |
Compound literals ((struct file_ops){ .read = impl }) | Supported |
Conditional (ops->read = cond ? a : b) | Both targets captured |
Copied to local variable (fn = ops->read; fn()) | Not tracked |
Whole-struct pass (register_ops(&ops) with later field call) | Not tracked (requires alias analysis) |
| Anonymous structs | Not tracked (no compound name) |