Hi,
I’m making an SDL_Init function and I’m playing around with keeping function pointers in a dynamic array to call upon later. Basically seeing if I can implement an on-demand defer
.
SDLCleanupWrapper :: proc "c" (ctx: ^AppStore) //cleanup function can access appstore when it is wrapped in this proc
SDLCleanups :: [dynamic]SDLCleanupWrapper // make a list of them to dynamically destroy in order (reverse traversal)
SDLSetup::
proc (ctx: ^AppStore, windowFlags: sdl.WindowFlags)
->(SDLCleanups,SDLSetupError){
cleanups := SDLCleanups{}
// Initialize SDL
sdl_init_result := sdl.Init({.VIDEO})
if sdl_init_result != true {
fmt.eprintln("SDL3 initialization failed:", sdl.GetError())
return cleanups, .SDL3InitFailed
}
// Wrap SDL.Quit to accept AppStore parameter
append(&cleanups, proc "c" (app_ctx: ^AppStore) {
sdl.Quit()
})
///... more
return cleanups, nil
}
///
cleanups,errors := SDLSetup(&ctx,{.RESIZABLE})
if errors != nil {
fmt.eprintln("SDLSetup failed:", errors)
cleanupSDL(cleanups, &ctx)
return
}
I’m mostly programming in languages with closures, so I have the tendency to want to do something like this within SDLSetup:
add_cleanup :: proc( cleanupFn:SDLCleanupFn ){
append( &cleanups, proc "c" (app_ctx: ^AppStore) {cleanupFn(app_ctx)} )
}
Other than hand-writing as above, is there a way I can make cleanupFn visible to that scope?
And in general, is there a pattern that you rely on that fulfils what closures do you for elsewhere?
Thanks
I’ll try to answer the title question. Firstly and most importantly, why do you want to implement an on-demand defer
? What problem does this solve (that isn’t handled by defer
)? Merely from the example you’ve shown, you can accomplish all of that with the core language constructs, so I’m not sure what the goal is here.
What does the pattern in your post afford you over something like this?
setup(&ctx) or_return
defer teardown(&ctx)
Secondly, getting out of the closure mindset should be easy enough, since Odin does not have closures, only procedure pointers—there’s no way to automatically bind state to a procedure variable as this would require garbage collection. You can of course make a struct
that has a pointer to a procedure and a rawptr
to some data and pretend it’s a closure, but you’d have to manually call it with the data (and manage its deallocation, if needed) and question why you’re adding these extra layers in the first place.
Finally, I’ve spent enough time in JavaScript to have seen pyramidic constructions from asynchronous callback hell, so I personally avoid them as much as possible. Callbacks can be useful for user code where you provide some sort of API to co-operate with your own library (core:flags
has register_flag_checker
, for example), but I’ve often found them unneeded in my own code. I just write the logic that I need, inline.
1 Like
Isn’t defer limited to the scope it’s called in? I think maybe I shouldn’t have mentioned defer at all. The approach is mostly to be able to create a generalised InitSDL function and be able to save that as a library for future projects.
In the above code I’m storing a list of teardown functions that need to be called. Maybe I selectively enable features, or some aren’t available but the program can still run, so that one teardown function isn’t needed. So one would think a dynamic list of them might be useful… Maybe with SDL you can call destroy without init but maybe that’s not true of all libraries.
The motivation for the specific example is that when I come back to it in a few months add_cleanup(cleanupProc)
would have a little less boilerplate than the repeated code. Assuming that there is a simple implementation for it, it would be more readable and easier to refactor. That adds up when you do that at every opportunity.
Personally I think inline mega-main functions are hard to navigate so I like set things up more declaratively separate procedures per step.
The question was really a general one - I agree that nested callbacks are often annoying but sometimes you want to be able to delay a function call with the relevant data. I’m sure I could store what’s needed in a struct but maybe there’s a pattern that people have found that is particularly simple.
The question is what is the Odin-friendly way of doing that?
Assuming this is for several projects that all use similar plug-&-play-styled features from a library of your own, I thought about how I might write something along the lines of what you’ve described. Here’s a simplified example:
package main
import "core:fmt"
Feature :: enum {
Graphics,
Networking,
Sound,
}
Feature_Set :: bit_set[Feature]
Context :: struct {
enabled: Feature_Set,
}
network_is_available := false // TODO
setup :: proc(features: Feature_Set) -> (ctx: Context) {
if .Graphics in features {
ctx.enabled |= {.Graphics}
fmt.println("Graphics are on.")
}
if .Networking in features {
if network_is_available {
ctx.enabled |= {.Networking}
fmt.println("Networking online.")
} else {
fmt.println("Networking unavailable.")
}
}
// ...
return
}
teardown :: proc(ctx: ^Context) {
if .Graphics in ctx.enabled {
// ...
}
if .Networking in ctx.enabled {
// ...
}
// ...
}
main :: proc() {
ctx := setup({.Graphics, .Networking})
defer teardown(&ctx)
}
1 Like
Thank you. That’s a nice way of doing it.
I’m still curious about the deferred function-with-context though. If I really wanted to do it, what is a good approach to storing the state with an allocator?
I can see why without GC it gets hairy - would you need to refcount every part of the context? Maybe you can validate in some way before running but that probably relates to how you manage memory in the larger picture.
Would it help to have the stored functions be separated into ‘oneoff’ and ‘recurrent’ functions in a registry whose listings / contexts either automatically get cleaned up , or remain stored?
These questions are why I wonder if there is an emergent best practice in c-style programming relating to this stuff. I know that the natural response is ‘you’re holding it wrong’ but solving these questions will help me in the long run to understand how it all fits together.
First, the obvious: In order to “get out of the closure mindset”, you should reduce your reliance upon closures.
Second: If you have no idea what state your program will be in when it gets somewhere, then you probably need to rethink your program.
Third: Enums and structs exist for a reason. Use them. A closure is a data structure with a function and the data needed to call it. The enum will mark what function you need - there are a finite number of them, and you know what they are when you create them. The structs (a different one for each function) can hold all the necessary data. Use a tagged union if you’re really feeling adventurous.
Remember, you knew what all the data was when you created the closure. This is the true message of Feoramund’s snippet. Don’t use function pointers when you can just select the proper one with a switch or series of if statements. Your code will be much more readable.
Fourth: Go read this thread: Closures are a poor man’s objects, and vice versa.
Now consider that Odin has neither closures nor objects. If your style of programming requires these, then Odin may simply not be a good fit for you at this time. But also consider that Odin doesn’t have these because they aren’t truly necessary. There are only data and functions that act on that data. Everything else is style and preference.
And objects are a poor man’s struct 
I actually code with data separate from functions when I’m using Typescript or Nim or whatever. And I like using statecharts for point 2 you mention.
But it’s also true that coding purely imperatively with big switch statements is extremely verbose sometimes, especially if you aren’t relying on macros. As with any style it has to be seen as a tradeoff. I get that there’s almost a brutalist philosophy to Odin/Zig but that doesn’t mean I have to play along!
I take your advice that it might go against the grain of the language. People do this function pointer stuff in C too I’m sure, I just want to know some approaches to it. Odin is a hobby language for me, not a production language so it’s a bit of a thought experiment.
I think the tone on programming forums in general could be a bit more exploratory /curious for want of a better word, and a bit less didactic. I’m interested in the problem, not being told the problem is not worth thinking about, that’s up to me…
Though I’m aware asking for help might bring that out in people, and that I specifically asked how /not/ to use closures. But just trying to have a conversation. Thanks for taking the time to answer regardless.
1 Like
My usual working language is Nim.
But I’m a language geek, and tend to explore a lot of different styles and paradigms. There are trade-offs to all of them, and we all have our own opinions and biases.
With the questions on a forum like this, you seldom have any idea of the background knowledge level of the questioner. So I tend to give very basic, foundational answers.
As for your particular question, your code snippet is about how I’d go about it myself, if I wanted to do this in a closure-like fashion. Traversing a list is about the most straightforward way I can think of, if you’re going to potentially have multiple finishing tasks to keep track of. Especially if they’re order sensitive and subjective to the particular task.
To me, both styles are about the same complexity. It’s just a matter of where you put the complexity at. Closures front load it, enums back load it. In the end, as long as it works and is readable, it doesn’t really matter. Odin is optimized to use the enum method, but lets you “roll your own” closures if you want to. That’s one of the things I like about it.