moon-chilled/Defer

Make it more portable?

nicowilliams opened this issue · 14 comments

goto *_deferrals[--_num_deferrals] is not portable C :(

There is good news though: you could use a Duff's device so that each deferral is a switch case and then you can while (_num_deferrals) loop around the switch. Not unlike async.h or Simon Tatham's C co-routines.

There is a portable version using setjmp/longjmp (look at the second half of the file); unfortunately, because longjmp tries to be clever, it causes problems when optimizations are enabled, and with dynamic stack allocations.

I did attempt a switch-based version, but was unsuccessful.

Leaving this open as a tracking issue for portability, though.

The other problem with a switch/case-based version is that you need to have another macro to put a closing brace at the end of the function. Which is not a huge deal, but it still adds friction and makes the library less 'natural' to use.

Yeah, I thought of the closing brace, but I'm ok with that. I can't use this with the setjmp()/longjmp() approach, and I can't use GCC/clang extensions. I'm also not sure I'd use it if it used a Duff's device either, but I am sure I need this :)

Oh, I think I see the problem with a Duff's device...

EDIT: Nah, made it work! PR submitted! Feel free to reject, naturally. And, yes, sadly, I did need BeginDeferral/EndDeferral macros :(

Can you elaborate on why you can't use the gcc extensions? Using MSVC? Or on an embedded platform?

This can be implemented safely using relatively trivial assembly routines; if that would suffice, let me know the platform and I can whip something up.

Yeah, MSVC :(

So if I make a version which uses amd64 assembly (masm syntax, I guess?), would that work for you?

Don't waste your time on that yet. If I could use this, I'd use it in Heimdal), but that would require consensus among us maintainers. I did the Duff's device thing for the fun of it. Thanks!

I've figured out how to make it work with MSVC using ASM. Essentially we need two macros, TakeLabelAddress(destination, label_name) and GotoLabelAddress(addr) that look like this:

#define TakeLabelAddress(dest, l) \
    do { void *_label; __asm{ mov [_label],offset l }; dest = _label; } while(0)
	
#define GotoLabelAddress(a) do { void *_label = (a); __asm{ jmp _label } } while (0)

A simplified defer.h for just MSVC (i.e., w/o #ifdef spaghetti) looks like this:

#ifndef DEFER_H
#define DEFER_H

#ifndef DEFER_MAX_DEFERRED_STATEMENTS
# define DEFER_MAX_DEFERRED_STATEMENTS 32
#endif

#define TakeLabelAddress(dest, l) \
    do { void *_labelv; __asm{ mov [_labelv],offset l }; dest = _labelv; } while(0)
	
#define GotoLabelAddress(a) do { _label = (a); __asm{ jmp _label } } while (0)


#define Deferral \
unsigned char _num_deferrals = 0; \
void *_defer_return_loc = 0, *_deferrals[DEFER_MAX_DEFERRED_STATEMENTS] = {0};

# define Defer(block) _Defer(block, __COUNTER__)
# define Return _Return(__COUNTER__)

#define _defer_tokpaste(a, b) a ## b

#define _Defer(block, n) do { \
	TakeLabelAddress(_deferrals[_num_deferrals++], _defer_tokpaste(_defer_ini, n)); \
	if (0) { \
		_defer_tokpaste(_defer_ini, n): \
		block; \
		if (_num_deferrals) { \
			GotoLabelAddress(_deferrals[--_num_deferrals]); \
		} else { \
			GotoLabelAddress(_defer_return_loc); \
		} \
	} \
} while (0)

#define _Return(n) \
	if (_num_deferrals) { \
		TakeLabelAddress(_defer_return_loc, _defer_tokpaste(_defer_fini_, n)); \
		GotoLabelAddress(_deferrals[--_num_deferrals]); \
	} \
\
	_defer_tokpaste(_defer_fini_, n): \
	return


#endif /*DEFER_H*/

Yes, I actually made pretty much that, when I was testing initial versions. But I canned it because __asm only works with 32-bit msvc, and I don't really care about 32-bit x86.

It doesn't work for 64-bit??

Ugh, you're right.

Old issue, but I've recently implemented an efficient, resource and user friendly solution in pure C99/C89.

  • Allows the user to use natural scope syntax with starting / ending curly brackets. No end-macro. Uses an almost unknown syntactic feature of switch to achieve this (curly bracket may be moved to after the first case ':').
  • Minimal stack usage, i.e. 4 bytes per defer (or whatever to store a __LINE__ value).
int process(int x) {
    printf("\nx=%d:\n", x);

    c_scope {
        c_defer({ puts("one"); });
        if (x < 0) c_return -1;
        
        c_defer({ puts("two"); });
        if (x > 0) continue;  // break out
        puts("OK");
    }

    puts("DONE");
    return 0;
}

int main() {
    process(0);
    process(1);
    process(-1);
}
x=0:
OK
two
one
DONE

x=1:
two
one
DONE

x=-1:
one

Note: I don't think any existing defer library implementation (including mine) supports nested defer-scopes. This is maybe the main reason it needs language support, along with the easy mistake to type return instead of c_return. This library solution is in any case a highly valuable tool in the toolbox. Some implementations will compile when creating nested defer scopes, but won't work correctly, e.g. for return. This implementation fails to compile in that case.

I have updated the implementation with two additional defer scope types, one which allows you to add one defer scope level inside a c_scope, called c2_scope. The other is cs_scope which allows you to have multiple defer scopes in sequence within a function (limitation of c_scope). These are setjmp-based and slow. c_scope is still the primary to use.