System designHow-to

Member function pointer as a GUI callback: the static trampoline

A GUI callback wants a plain function pointer, not a member function pointer. The static trampoline and user-data slot bridge them; code and test included.

LumaSpec Vision9 min read Share on LinkedIn

On this page 10 sections
  1. 01Why does a C callback refuse a member function pointer?
  2. 02What is the static trampoline that fixes it?
  3. 03How do you wire a threshold trackbar to the member, step by step?
  4. 04What if the registration function has no user-data slot?
  5. 05Why should you never cast the member pointer to a plain function pointer?
  6. 06Which thread does the callback run on, and what must it survive?
  7. 07How do you prove the callback fires before you trust the image?
  8. 08When is the C API the wrong layer to fight?
  9. 09What hardware runs this pattern on the bench?
  10. 10Questions

Your filtering code lives in a class, and the window toolkit or camera SDK that should trigger it takes a plain C function pointer, so the compiler refuses your member function before the program ever builds. The error looks fatal and the fix is three lines. This guide shows the static trampoline pattern: a static member with the exact signature the C API expects, your object carried through the user-data slot, and a cast that forwards the event into the real member function. You get working code for a threshold trackbar wired to a ClassModel member, the fallback for APIs with no user-data slot, the reason a cast is never an option, and the drag test that proves the callback fires.

Why does a C callback refuse a member function pointer?

Because a member function pointer is not a plain address: void (ClassModel::)(int) must also say how to reach this, so where the API wants void ()(int) the compiler answers cannot convert 'void (ClassModel::**)(int)' to 'void (*)(int)' for argument 5, the callback parameter.

A non-static member is always called through an object: image_1.thresholdFilter(127) compiles to a call that carries image_1's address as this. The type void (ClassModel::)(int) encodes the class as well as the signature, so it is a different type from void ()(int), and no implicit conversion exists between them. The C API was compiled without any knowledge of classes, so its callback parameter is a single code address and nothing else. The refusal at build time is the friendly outcome: the alternative is a call executed with garbage for this.

The same wall greets every C-style registration: a trackbar callback such as the one in cvCreateTrackbar, a mouse callback, or the frame callback of an area scan camera SDK. Same refusal, same fix.

What is the static trampoline that fixes it?

A static trampoline is a static member function with exactly the parameter list the C API expects plus one void* that carries your object; inside, a single cast hands the event to the real member function.

A static member function has no this of its own, so its address is an ordinary function address and the type system hands it to C without complaint. The instance travels as data instead: the registration call stores your void* next to the function pointer, and the library hands that same pointer back on every event. The static knows the class, the slot knows the instance, and the toolkit never sees a member pointer. Where the slot sits varies — usually the last parameter of the registration call — but the shape never does.

void thresholdFilter(int threshold);            // member: has this, the work you want done
static void onThreshold(int pos, void* user);   // static: no this, what the C API can store

GUI preview loops use the same wiring, because paint events and camera frames arrive from different directions; the camera live preview in GUI applications guide takes that loop apart.

How do you wire a threshold trackbar to the member, step by step?

Five steps: declare the static next to the member, forward the position, register the static with your object's address in the user-data slot, keep the object alive as long as the window, then prove it with the drag test.

The worked case: ClassModel holds an 8-bit gray image, thresholdFilter(int) thresholds it, and the slider spans 0 to 255.

  1. Declare the member and the static side by side. The member keeps its signature; the static repeats the API's callback signature and adds the slot last:
class ClassModel {
    IplImage* imageBase;
    IplImage* imageBuffer;
public:
    explicit ClassModel(IplImage* image);
    ~ClassModel();
    IplImage* getImgBuffer();
    void thresholdFilter(int threshold);           // the work
    static void onThreshold(int pos, void* user);  // the trampoline
};
  1. Define the trampoline as a cast and a forward. The whole mechanism is two lines of body:
void ClassModel::onThreshold(int pos, void* user) {
    static_cast<ClassModel*>(user)->thresholdFilter(pos);
}
  1. Register the static, never a member pointer, and pass the object as the user-data argument:
int seuil = 128;  // trackbar position and start value, 0 to 255
// create_trackbar is the stand-in name for your toolkit's registration call,
// with its user-data slot as the last argument:
create_trackbar("Seuil", "Base NVG", &seuil, 255, &ClassModel::onThreshold, &image_1);

Where a library ships two versions of a registration function, one with the slot and one without, call the one with it.

  1. Keep the object alive as long as the window. The trampoline fires after registration returns, so scope image_1 around the event loop and close the window before it goes out of scope.

  2. Budget one static per callback registration, two lines of body each.

What if the registration function has no user-data slot?

Then the API gives you nothing to carry the object in, so carry it yourself: a file-scope pointer set before the window opens covers the single-object case, and a registry keyed by an identifier covers several.

The classic five-argument trackbar call is exactly that case: trackbar name, window name, a value pointer, the maximum and a void (*)(int) callback, with nowhere to hang an object.

static ClassModel* g_model = nullptr;  // the instance this window drives

void onThreshold(int pos) {            // plain void (*)(int): what the API stores
    if (g_model != nullptr) {
        g_model->thresholdFilter(pos);
    }
}
// in main(), before the event loop:
int seuil = 128;
g_model = &image_1;
cvCreateTrackbar("Seuil", "Base NVG", &seuil, 255, onThreshold);

Pass the function's bare name — a static member or a free function — never a member pointer. The global binds one window to one instance, which is acceptable for a single-window tool and a trap at anything bigger: for several objects, move to an API revision that has the slot, or keep a map from whatever identifier the callback does receive to instances and let one dispatcher look the object up.

Why should you never cast the member pointer to a plain function pointer?

Never: a member function called through a cast pointer runs with no valid this, which is undefined behavior — a crash now, or code that works until the next compiler or build setting changes.

A pointer-to-member-function is wider than a plain function pointer for a reason, often 16 bytes against 8 in 64-bit GCC and Clang builds, because it stores the code address plus the adjustment that computes this; some compilers grow it further under multiple inheritance. reinterpret_cast erases that structure, and the call then executes with whatever the register happens to hold as this: a crash, a corrupted image, or code that passes today's run and fails on the next compiler update. The conversion error at the top of this guide is the type system refusing an impossible call, which is exactly what you want from it.

Which thread does the callback run on, and what must it survive?

The GUI or SDK event thread, not your processing loop: keep the member quick, guard any state it shares, and copy image data out before the library reuses the buffer.

A trackbar callback fires on the window's event thread; a camera SDK frame callback fires on the acquisition thread. Either way it interrupts the program wherever the event pump happens to be. Store the position and post the heavy filter to the processing thread, guard any buffer both sides touch, and never hold a pointer into a frame the SDK owns: copy it. Two failure modes from real integrations show what happens otherwise: a buffer reused under your code appears as torn frames in a camera callback, and shared state touched from two threads ends as the event camera frame generation crash fixed by thread safety in the callback.

How do you prove the callback fires before you trust the image?

Drag the trackbar end to end on an 8-bit gray image and check three frames: almost all white at position 0, all black at 255, and a near half-and-half split at 127.

The member thresholds with the binary mode and a maximum of 255, so every output pixel comes from one comparison:

out = 255 where source > threshold, else 0

The slider offers 256 positions, 0 through 255. Take two source pixels, 140 and 90. At a threshold of 127, 140 exceeds it and turns white while 90 does not and stays black. At 0, both pixels exceed the threshold, so the image goes almost entirely white and only true-zero pixels stay black. At 255, nothing exceeds the threshold, so the whole image goes black. Near-white at 0, all black at 255, split close to half at 127: those three frames prove the wiring. If the image never changes, the fault sits upstream of the filter: check that the slot or g_model was set before registration, that you passed the static's name and not a member pointer, and that the trackbar's window name matches the window you show. For proof independent of the image, keep a counter in the trampoline and print it: it should climb as you drag.

When is the C API the wrong layer to fight?

When more than a handful of callbacks need their own object: wrap the C layer once in a class that keeps a map from tokens to std::function and exposes one static dispatcher, and each later callback is a lambda.

Two or three hand-written statics are fine. Ten are a maintenance tax with the same two lines copy-pasted everywhere. The adapter keeps a std::map from token to std::function, its single static dispatcher looks the token up in the slot and calls the stored function, and every later registration is one line. The trampoline pattern stays underneath unchanged; only the bookkeeping moves.

The constraint is not unique to C++ hosts. Any language that calls a C DLL meets the same plain-function-pointer wall, and whether a LabVIEW VI can serve as the callback for a DLL function is its own documented question. For the same wall crossed from the other side, read calling a camera SDK DLL from LabVIEW: callbacks, buffers and snap and the camera SDK callback wrapper DLL that bridges events with C.

What hardware runs this pattern on the bench?

Anything you like: the trampoline is pure software, so choose the camera by sensor, resolution and interface — GigE for cable runs up to 100 m, USB3 for the roughly 3 m around a bench — and give it a controller with one spare LAN port per camera.

Nothing in the callback code changes between a bench PC and a vision controller mounted next to the line, so let the sensor decide. Shortlist cameras in the camera finder, then wire camera, controller and lighting outputs together in the system configurator. More integration patterns sit on the systems topic page, and a stubborn SDK callback is worth bringing to the contact page.

Questions

Does a capturing lambda work as a C callback?

No. Only a capture-free lambda converts to a plain function pointer, and without captures it cannot reach your object — a trampoline that forgot the slot. Capturing lambdas need std::function, so they only work in hosts that accept one. For a C API, keep the static trampoline and pass the object through the user-data slot.

Can one static trampoline serve two different objects?

Yes, when the API has a user-data slot: register the same static twice, once with &model_a and once with &model_b, and every event casts its own slot to the right instance. Without a slot it cannot — a static has no per-object storage — so use a registry keyed by an identifier the callback receives, or wrap the API.

Is a static member function allowed to touch private members?

Yes. A static member function of ClassModel has access to ClassModel's private members, so the two-line trampoline can call a private threshold method directly. A free function cannot, which is the main reason to prefer a static member over a file-scope function when both are possible.

What changes when the callback has more parameters?

Nothing structural. The static repeats the API's exact parameter list, adds the void* slot where the API puts it, and forwards every argument unchanged: a mouse-style callback carrying coordinates and flags forwards five values plus the slot the same way the trackbar forwards one. Only the signature changes; the cast-and-forward body stays identical.