[HN Gopher] Simplest C++ Callback, from SumatraPDF
___________________________________________________________________
Simplest C++ Callback, from SumatraPDF
Author : jandeboevrie
Score : 59 points
Date : 2025-06-15 17:26 UTC (5 hours ago)
(HTM) web link (blog.kowalczyk.info)
(TXT) w3m dump (blog.kowalczyk.info)
| not-so-darkstar wrote:
| https://web.archive.org/web/20250615181542/https://blog.kowa...
| kjksf wrote:
| It's back up now.
|
| Somehow my blog server got overwhelmed and requests started
| taking tens of seconds. Which is strange because typically it's
| under 100ms (it's just executing a Go template).
|
| It's not a CPU issues so there must be locking issue I don't
| understand.
| waynecochran wrote:
| A small kitten dies every time C++ is used like its 1995.
| void (*fn)(void*, T) = nullptr;
| plq wrote:
| Unless you mutter the magic incantation "C compatibility" while
| doing it
| zabzonk wrote:
| did nullptr exist in c++ back in 1995 - i can't remember
| trealira wrote:
| Nope, it was introduced in C++11, along with the type
| std::nullptr_t. Before that, you either used 0 or NULL, which
| was a macro constant defined to be 0.
| tom_ wrote:
| And another one dies every time you need to step through a call
| to std::function. Whatever you do, the kittens are never going
| to escape.
| JeanMarcS wrote:
| Don't know about the code subtilities, but SumatraPDF is a gift
| for viewing PDF on MS Windows. So big thanks to the author !
| jitans wrote:
| Why don't use fu2? https://naios.github.io/function2/
| kjksf wrote:
| One, I didn't know about it.
|
| Two, my main objective is extreme simplicity and
| understandability of the code.
|
| I explicitly gave up features of std::function for smaller code
| that I actually understand.
|
| fu2 seems to be "std::function but more features".
| mdaniel wrote:
| SumatraPDF is outstanding software. But I'm actually surprised to
| hear that it seems to be written in C++ ... I dunno, kind of like
| "by default?" And a blog post hand rolling callback functions
| using structs and a bunch of pointers seems to double down on:
| are you sure this language is getting you where you want to go?
| kjksf wrote:
| As opposed to?
|
| Today, if I was starting from scratch, I would try zig or odin
| or maybe even Go.
|
| But SumatraPDF started 15 years. There was nothing but C++. And
| a much different C++ that C++ of today.
|
| Plus, while my own code is over 100k lines, external C / C++
| libraries are multiple of that so (easy) integration with C /
| C++ code is a must.
| mdaniel wrote:
| I didn't know how to correctly package my comment as not
| criticizing, and that's half of why I opened with "is
| outstanding software." I genuinely believe that, I'm deeply
| grateful for you releasing SumatraPDF into the world, and it
| makes my life better. Truly, I am thankful
|
| I hear you about "back in my day," but since as best I can
| tell it's just your project (that is, not a whole team of 50
| engineers who have to collaborate on the codebase) so you are
| the audience being done a disservice by continuing to battle
| a language that hates you
|
| As for the interop, yes, since the 70s any language that
| can't call into a C library is probably DoA but that list
| isn't the empty set, as you pointed out with the ones you've
| actually considered. I'd even _suspect_ if you tried Golang
| it may even bring SumatraPDF to other platforms which would
| be another huge benefit to your users
| mhd wrote:
| > I'd even suspect if you tried Golang it may even bring
| SumatraPDF to other platforms which would be another huge
| benefit to your users
|
| Probably by using a cross-platform toolkit written in C++.
| kjksf wrote:
| Don't worry about being nice, 15 years doing open source
| develops a thick skin.
|
| But you didn't respond: which language should I use?
|
| Go didn't exist when I started SumatraPDF.
|
| And while I write pretty much everything else in Go and
| love the productivity, it wouldn't be a good fit.
|
| A big reason people like Sumatra is that it's fast and
| small. 10 MB (of which majority are fonts embedded in the
| binary) and not 100MB+ of other apps.
|
| Go's "hello world" is 10 MB.
|
| Plus abysmal (on Windows) interop with C / C++ code.
|
| And the reason SumatraPDF is unportable to mac / linux is
| not the language but the fact that I use all the Windows
| API I can for the UI.
|
| Any cross-platform UI solution pretty much require using
| tens of megabytes of someone else's reimplementation of all
| the UI widgets (Qt, GTK, Flutter) or re-implementing a
| smaller subset of UI using less code.
| sitzkrieg wrote:
| sumatrapdf not being cross platform is a great feature,
| maximizing the use of intended platform. win32api is
| great. thank you for that
| cherryteastain wrote:
| Why not just pass around an
| std::pair<void(*)(FuncData*), std;:unique_ptr<FuncData>>
|
| at this stage? This implementation has a bunch of performance and
| ergonomics issues due to things like not using perfect forwarding
| for the Func1::Call(T) method, so for anything requiring copying
| or allocating it'll be a decent bit slower and you'll also be
| unable to pass anything that's noncopyable like an
| std::unique_ptr.
| kjksf wrote:
| I don't know fancy C++ so I don't understand your point about
| perfect forwarding.
|
| But I do know the code I write and you're wrong about
| performance of Func0 and Func1. Those are 2 machine words and
| all it takes to construct them or copy them is to set those 2
| fields.
|
| There's just no way to make it faster than that, both at
| runtime or at compile time.
|
| The whole point of this implementation was giving up fancy
| features of std::function in exchange for code that is small,
| fast (both runtime and at compilation time) and one that I 100%
| understand in a way I'll never understand std::function.
| cherryteastain wrote:
| In this function void Call(T arg) const {
| if (fn) { fn(userData, arg); }
| }
|
| Say you pass something like an std::vector<double> of size 1
| million into Call. It'll first copy the std::vector<double>
| at the point you invoke Call, even if you never call fn.
| Then, if fn is not nullptr, you'll then copy the same vector
| once more to invoke fn. If you change Call instead to
| void Call(T&& arg) const { if (fn) {
| fn(userData, std::forward<T>(arg)); } }
|
| the copy will not happen at the point Call is invoked.
| Additionally, if arg is an rvalue, fn will be called by
| moving instead of copying. Makes a big difference for
| something like std::vector<double> foo();
| void bar(Func1<std::vector<double>> f) { auto v =
| foo(); f(std::move(v)); }
| OskarS wrote:
| > But I do know the code I write and you're wrong about
| performance of Func0 and Func1. Those are 2 machine words and
| all it takes to construct them or copy them is to set those 2
| fields.
|
| You also have to heap allocate your userData, which is
| something std::function<> avoids (in all standard
| implementations) if it's small enough (this is why the
| sizeof() of std::function is larger than 16 bytes, so that it
| can optionally store the data inline, similar to the small
| string optimization). The cost of that heap allocation is not
| insignificant.
|
| If I were doing this, I might just go the full C route and
| just use function pointers and an extra "userData" argument.
| This seems like an awkward "middle ground" between C and C++.
| _randyr wrote:
| I'm not a C++ programmer, but I was under the impression that
| closures in c++ were just classes that overload the function call
| operator `operator()`. So each closure could also be implemented
| as a named class. Something like: class
| OnListItemSelected { OnListItemSelectedData data;
| void operator()(int selectedIndex) { ... } }
|
| Perhaps I'm mistaken in what the author is trying to accomplish
| though?
| OskarS wrote:
| Indeed, that is exactly the case, lambdas are essentially
| syntax sugar for doing this.
|
| The one thing the author's solution does which this solution
| (and lambdas) does not is type erasure: if you want to pass
| that closure around, you have to use templates, and you can't
| store different lambdas in the same data structure even if they
| have the same signature.
|
| You could solve that in your case by making `void operator()`
| virtual and inheriting (though that means you have to heap-
| allocate all your lambdas), or use `std::function<>`, which is
| a generic solution to this problem (which may or may not
| allocate, if the lambda is small enough, it's usually optimized
| to be stored inline).
|
| I get where the author is coming from, but this seems very much
| like an inferior solution to just using `std::function<>`.
| spacechild1 wrote:
| Exactly! And if you need type erasure, you can just store it in
| a std::function.
|
| > OnListItemSelectedData data;
|
| In this case you can just store the data as member variables.
| No need for defining an extra class just for the data.
|
| As I've written elsewhere, you can also just use a lambda and
| forward the captures and arguments to a (member) function. Or
| if you're old-school, use std::bind.
| noomen wrote:
| I just want to thank SumatraPDF's creator, he literally saved my
| sanity from the evil that Adobe Acrobat Reader is. He probably
| saved millions of people thousands of hours of frustration using
| Acrobat Reader.
| pjmlp wrote:
| Another example of NIH, better served by using the standard
| library.
| spacechild1 wrote:
| > I've used std::function<> and I've used lambdas and what pushed
| me away from them were crash reports.
|
| In danger of pointing out the obvious: std::function does note
| require lambdas. In fact, it has existed long before lambdas
| where introduced. If you want to avoid lambdas, just use
| std::bind to bind arguments to regular member functions or free
| functions. Or pass a lambda that just forwards the captures and
| arguments to the actual (member) function. There is no reason for
| regressing to C-style callback functions with user data.
| mandarax8 wrote:
| std::bind is bad for him for the same reasons std::function is
| bad though
| spacechild1 wrote:
| Why? If the bound (member) function crashes, you should get a
| perfectly useable crash report. AFAIU his problem was that
| lambdas are anonymous function objects. This is not the case
| here, because the actual code resides in a regular (member)
| function.
| dustbunny wrote:
| Does a stack trace from a crash in a bound function show
| the line number of where the bind() took place?
| delusional wrote:
| Assuming the stack trace is generated by walking up the
| stack at the time when the crash happened, nothing that
| works like a C function pointer would ever do that.
| Assigning a a pointer to a memory location doesn't
| generate a stack frame, so there's no residual left in
| the stack that could be walked back.
|
| A simple example. If you were to bind a function pointer
| in one stack frame, and the immediately return it to the
| parent stack frame which then invokes that bound pointer,
| the stack that bound the now called function would
| literally not exist anymore.
| spacechild1 wrote:
| No, but neither does the author's solution.
| kjksf wrote:
| I did use bind earlier in SumatraPDF.
|
| There are 2 aspects to this: programmer ergonomics and other
| (size of code, speed of code, compilation speed,
| understandability).
|
| Lambdas with variable capture converted to std::function have
| best ergonomics but at the cost of unnamed, compiler-generated
| functions that make crash reports hard to read.
|
| My Func0 and Func1<T> approach has similar ergonomics to
| std::bind. Neither has the problem of potentially crashing in
| unnamed function but Func0/Func1<T> are better at other
| (smaller code, faster code, faster compilation).
|
| It's about tradeoffs. I loved the ergonomics of callbacks in C#
| but I working within limitations of C++ I'm trying to find
| solutions with attributes important to me.
| mandarax8 wrote:
| What he shows here is 75% of c++26's std::function_ref. It's
| mainly missing variadic arguments and doesn't support all types
| of function objects.
|
| https://github.com/TartanLlama/function_ref/blob/master/incl...
| kjksf wrote:
| I can honestly say that I couldn't write that thing in 100
| years.
|
| I can't even read it.
|
| That's the fundamental problem with C++: I've understood pretty
| much all Go code I ever looked at.
|
| The code like the above is so obtuse that 0.001% of C++
| programmers is capable of writing it and 0.01% is capable of
| understanding it.
|
| Sure, I can treat it as magic but I would rather not.
| spacechild1 wrote:
| Do you understand how your compiler works? Shouldn't you be
| writing assembly instead? You can't understand all internals
| and that's perfectly fine.
|
| Why do you even care how std::function is implemented?
| (Unless you are working in very performance critical or
| otherwise restricted environments.)
| kjksf wrote:
| I've listed several reasons why I decided to write and use
| this implementation: - better call stacks
| in crash reports - smaller and faster at runtime
| - faster compilation because less complicated, less
| templated code - I understand it
|
| So there's more to it that just that one point.
|
| Did I loose useful attributes? Yes. There's no free lunch.
|
| Am I going too far to achieve small, fast code that
| compiles quickly? Maybe I do.
|
| My code, my rules, my joy.
|
| But philosophically, if you ever wonder why most software
| today can't start up instantly and ships 100 MB of stuff to
| show a window: it's because most programmers don't put any
| thought or effort into keeping things small and fast.
| spacechild1 wrote:
| Oh, I definitely agree with some of your other points,
| just not the one I argued against.
|
| BTW, I would also contest that your version is faster at
| runtime. Your data always allocated on the heap.
| Depending on the size of the data, std::function can
| utilize small function optimization and store everything
| in place. This means there is no allocation when setting
| the callback and also better cache locality when calling
| it. Don't make performance claims without benchmarking!
|
| Similarly, the smaller memory footprint is not as clear
| cut: with small function optimization there might be
| hardly a difference. In some cases, std::function might
| even be smaller. (Don't forget about memory allocation
| overhead!)
|
| The only point I will absolutely give you is compilation
| times. But even there I'm not sure if std::function is
| your bottleneck. Have you actually measured?
| mandarax8 wrote:
| Yeah it's a shame that to go from your idea to something
| that's 'general' (ie just some arbitrary arguments) you need
| to write this arcane garbage.
| commandersaki wrote:
| I always love this author's writing style, his articles are pure
| bliss to read.
| mwkaufma wrote:
| The lengths some go to avoid just using a bog-standard virtual
| function.
| kjksf wrote:
| I actually used the "virtual function" approach earlier in
| SumatraPDF.
|
| The problem with that is that for every type of callback you
| need to create a base class and then create a derived function
| for every unique use.
|
| That's a lot of classes to write.
|
| Consider this (from memory so please ignore syntax errors, if
| any): class ThreadBase { virtual
| void Run(); // ... } class
| MyThread : ThreadBase { MyData* myData;
| void Run() override; // ... }
| StartThread(new MyThread());
|
| compared to: HANDLE StartThread(const Func0&,
| const char* threadName = nullptr); auto fn =
| MkFunc0(InstallerThread, &gCliNew); StartThread(fn,
| "InstallerThread");
|
| I would have to create a base class for every unique type of
| the callback and then for every caller possibly a new class
| deriving.
|
| This is replaced by Func0 or Func1<T>. No new classes, much
| less typing. And less typing is better programming ergonomics.
|
| std::function arguably has slightly better ergonomics but
| higher cost on 3 dimension (runtime, compilation time,
| understandability).
|
| In retrospect Func0 and Func1 seem trivial but it took me years
| of trying other approaches to arrive at insight needed to
| create them.
| mgaunard wrote:
| Should have just implemented his own std::function with the
| simplicity and performance trade-off he wanted.
| akdev1l wrote:
| I don't really understand what problem this is trying to solve
| and how the solution is better than std::function. (I understand
| the issue with the crash reports and lambdas being anonymous
| classes but not sure how the solution improved on this or how
| std::function has this problem?)
|
| I haven't used windows in a long time but back in the day I
| remember installing SumatraPDF to my Pentium 3 system running
| windows XP and that shit rocked
___________________________________________________________________
(page generated 2025-06-15 23:00 UTC)