Attribute names in macros

When I create a macro and the definition contains an Axoloti attribute name my patch will often fail to compile. I was hoping that someone could tell me why, and whether there are any workarounds. I’ve searched the axo forum archive for information without success. Googling “C macro scope error” turns up lots of potentially relevant information, but I’m not sure how applicable it is, partly because I don’t really know what axo attributes ARE in the taxonomy of things codable in C. I’m using an Axoloti, but I assume that this is relevant to Ksoloti object coding.

If I create an object in a blank patch with a spinner attribute called “giraffe” and set it to 4, the following works fine in that object’s Init Code, printing “4” to the console when I go live:

#define BANANA	attr_giraffe
LogTextMessage("%d", BANANA);

But I don’t usually want to include macro definitions in the same objects where the macros are used. I want them in the Local Data block of a separate object with all my other macros. And if I create a new object and move the BANANA define to its Local Data block, my patch won’t compile. The console says “error: ‘attr_giraffe’ was not declared in this scope”.

I haven’t had this problem with any other kind of code in a macro definition. Normally a macro will work even if the text on the right hand side of the define makes no sense in the context of the object where it is coded (e.g. even if it contains the names of variables that haven’t been declared there).

I’ve also found that, regardless of where the define is coded, concatenation of attribute names by means of ## always results in a compilation failure. E.g.

#define LEMON(x,y)	x ## y
LogTextMessage("%d", LEMON(attr_gir, affe));

The error message is the same. Anyone know what’s going on?

Hi, I am not sure I understand what exact result you’re after, but I’ll try to explain as much as I can.

  • Attributes are just placeholders that will be replaced with the selected value by the Patcher’s code generator. So if I create an attr_giraffe in one object and set its value to, say, “4” or “GPIOA,1”, the code generator will literally parse that object when you click on “live”, and it will replace all attr_giraffe substrings it can find with 4 or GPIOA,1. Note that this replacement applies only inside the object that contains the attribute.

  • If I am not mistaken, defines should be valid across the whole patch. So if you do

#define BANANA	attr_giraffe

other objects in the patch should technically be able to pick up the define BANANA, you’re correct there.

BUT there is a certain instantiation and execution order of objects, from left to right, and line by line from top to bottom. (IIRC the object’s title bar is seen as an anchor for the object’s position).

So if you go like this:


You get the following error:

If we look at the auto generated xpatch.cpp (in home/[user]/ksoloti/build, or home/[user]/ksoloti/[version]/build since 1.1.0), we can see what has really happened:

non-working xpatch.cpp
/*
 * Generated using Ksoloti Patcher v1.1.0-4-g7ba2ed26 on Windows 10
 * File: untitled
 * Generated: Mon, 14 Apr 2025 13:27:12 +0800
 */

#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-parameter"


#define MIDICHANNEL 0 // DEPRECATED

int32buffer AudioInputLeft, AudioInputRight, AudioOutputLeft, AudioOutputRight;

void xpatch_init2(uint32_t fwid);

extern "C" __attribute__ ((section(".boot"))) void xpatch_init(uint32_t fwid) {
	xpatch_init2(fwid);
}

void PatchMidiInHandler(midi_device_t dev, uint8_t port, uint8_t status, uint8_t data1, uint8_t data2);

static void PropagateToSub(ParameterExchange_t* origin) {
	ParameterExchange_t* pex = (ParameterExchange_t*) origin->finalvalue;
	PExParameterChange(pex, origin->modvalue, 0xFFFFFFEE);
}

class rootc {
	public:

	static const uint16_t NPEXCH = 0;
	ParameterExchange_t PExch[NPEXCH];
	int32_t displayVector[3];
	static const uint8_t NPRESETS = 0;
	static const uint8_t NPRESET_ENTRIES = 0;
	static const uint8_t NMODULATIONSOURCES = 0;
	static const uint8_t NMODULATIONTARGETS = 0;
	int32_t PExModulationPrevVal[1][NMODULATIONSOURCES];

	/* Parameter instance indices */

	/* Object classes */
	class objectinstance_give__me__banana {
		public: // v1
		rootc *parent;

		/* Object Local Code Tab */
		int given = BANANA;

		public: void Init(rootc *_parent) {
			parent = _parent;
			
			/* Object Init Code Tab */

		}

		public: void Dispose() {

		}

		public: void dsp() {

		}

	};

	class objectinstance_define__banana {
		public: // v1
		rootc *parent;

		/* Object Local Code Tab */
		#define BANANA 1

		public: void Init(rootc *_parent) {
			parent = _parent;
			
			/* Object Init Code Tab */

		}

		public: void Dispose() {

		}

		public: void dsp() {

		}

	};


	/* Object instances */
	objectinstance_give__me__banana objectinstance_give__me__banana_i;
	objectinstance_define__banana objectinstance_define__banana_i;

	/* Net latches */

	static const uint32_t polyIndex = 0;

	static int32_t* GetInitParams(void) {
		static const int32_t p[NPEXCH] = {
		};
		return (int32_t*) &p[0];
	}

	static const int32_t* GetPresets(void) {
		static const int32_t p[NPRESETS][NPRESET_ENTRIES][2] = {
		};
		return &p[0][0][0];
	};

	void ApplyPreset(uint8_t index) {
		if (!index) {
			int32_t* p = GetInitParams();
			uint32_t i; for (i = 0; i < NPEXCH; i++) {
				PExParameterChange(&PExch[i], p[i], 0xFFEF);
			}
		}
		index--;
		if (index < NPRESETS) {
			PresetParamChange_t* pa = (PresetParamChange_t*) (GetPresets());
			PresetParamChange_t* p = &pa[index * NPRESET_ENTRIES];
			uint32_t i; for (i = 0; i < NPRESET_ENTRIES; i++) {
				PresetParamChange_t* pp = &p[i];
				if ((pp->pexIndex >= 0) && (pp->pexIndex < NPEXCH)) {
					PExParameterChange(&PExch[pp->pexIndex], pp->value, 0xFFEF);
				}
				else break;
			}
		}
	}

	static PExModulationTarget_t* GetModulationTable(void) {
		static const PExModulationTarget_t PExModulationSources[NMODULATIONSOURCES][NMODULATIONTARGETS] = {
		};

		return (PExModulationTarget_t*) &PExModulationSources[0][0];
	};

	/* Patch init */
	void Init() {
		uint32_t i, j;
		const int32_t* p;
		p = GetInitParams();

		for (j = 0; j < NPEXCH; j++) {
			PExch[j].value = p[j];
			PExch[j].modvalue = p[j];
			PExch[j].signals = 0;
			PExch[j].pfunction = 0;
		}

		int32_t* pp = &PExModulationPrevVal[0][0];
		for (j = 0; j < (1 * NMODULATIONSOURCES); j++) {
			*pp = 0; pp++;
		}

		displayVector[0] = 0x446F7841;
		displayVector[1] = 0;
		displayVector[2] = 0;
		objectinstance_give__me__banana_i.Init(this);
		objectinstance_define__banana_i.Init(this);

		uint32_t k; for (k = 0; k < NPEXCH; k++) {
			if (PExch[k].pfunction) {
				(PExch[k].pfunction)(&PExch[k]);
			}
			else {
				PExch[k].finalvalue = PExch[k].value;
			}
		}
	}


	/* Patch dispose */
	void Dispose() {
		objectinstance_define__banana_i.Dispose();
		objectinstance_give__me__banana_i.Dispose();
	}

	void __attribute__((optimize("-O2"))) clearBuffers(void) {
		uint32_t u;
		for(u=0; u < BUFSIZE; u++) {
			AudioOutputLeft[u] = 0;
			AudioOutputRight[u] = 0;
		}
	}

	/* Patch k-rate */
	void dsp(void) {
		uint32_t i;
		clearBuffers();

		/* <nets> */
		/* </nets> */

		/* <zero> */
		int32_t UNCONNECTED_OUTPUT;
		static const int32_t UNCONNECTED_INPUT = 0;
		static const int32buffer ZEROBUFFER = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
		int32buffer UNCONNECTED_OUTPUT_BUFFER;
		/* </zero> */

		/* <object calls> */
		objectinstance_give__me__banana_i.dsp();
		objectinstance_define__banana_i.dsp();
		/* </object calls> */

		/* <net latch copy> */
		/* </net latch copy> */
	}

	void MidiInHandler(midi_device_t dev, uint8_t port, uint8_t status, uint8_t data1, uint8_t data2) {
	}

};

static rootc root;

void PatchProcess(int32_t* inbuf, int32_t* outbuf) {
	uint32_t i;
	for (i = 0; i < BUFSIZE; i++) {
		/* AudioInputMode == A_STEREO */
		AudioInputLeft[i] = inbuf[(i<<1)] >> 4;
		AudioInputRight[i] = inbuf[(i<<1) + 1] >> 4;
	}

	root.dsp();

	for (i = 0; i < BUFSIZE; i++) {
		/* AudioOutputMode == A_STEREO */
		outbuf[(i<<1)] = __SSAT(AudioOutputLeft[i], 28) << 4;
		outbuf[(i<<1) + 1] = __SSAT(AudioOutputRight[i], 28) << 4;
	}
}

void ApplyPreset(uint8_t i) {
	root.ApplyPreset(i);
}

void PatchMidiInHandler(midi_device_t dev, uint8_t port, uint8_t status, uint8_t data1, uint8_t data2) {
	root.MidiInHandler(dev, port, status, data1, data2);
}

typedef void (*funcp_t) (void);
typedef funcp_t* funcpp_t;

extern funcp_t __ctor_array_start;
extern funcp_t __ctor_array_end;
extern funcp_t __dtor_array_start;
extern funcp_t __dtor_array_end;

void PatchDispose( ) {
	root.Dispose();

	{
		funcpp_t fpp = &__dtor_array_start;
		while (fpp < &__dtor_array_end) {
			(*fpp)();
			fpp++;
		}
	}
}

void xpatch_init2(uint32_t fwid) {
	if (fwid != 0xEC73A8DA) {
		// LogTextMessage("Patch firmware mismatch");
		return;
	}

	extern uint32_t _pbss_start;
	extern uint32_t _pbss_end;
	volatile uint32_t* p;
	for (p = &_pbss_start; p < &_pbss_end; p++) {
		*p = 0;
	}

	{
		funcpp_t fpp = &__ctor_array_start;
		while (fpp < &__ctor_array_end) {
			(*fpp)();
			fpp++;
		}
	}

	patchMeta.npresets = 0;
	patchMeta.npreset_entries = 0;
	patchMeta.pPresets = (PresetParamChange_t*) root.GetPresets();
	patchMeta.pPExch = &root.PExch[0];
	patchMeta.pDisplayVector = &root.displayVector[0];
	patchMeta.numPEx = 0;
	patchMeta.patchID = -2010510764;

	extern char _sdram_dyn_start;
	extern char _sdram_dyn_end;
	sdram_init(&_sdram_dyn_start, &_sdram_dyn_end);

	root.Init();

	patchMeta.fptr_applyPreset   = ApplyPreset;
	patchMeta.fptr_patch_dispose = PatchDispose;
	patchMeta.fptr_MidiInHandler = PatchMidiInHandler;
	patchMeta.fptr_dsp_process   = PatchProcess;
}

But if you put the define_banana object above give_me_banana, it compiles just fine.

Note that this is with the define in the Local Data tab. There is some confusion as to which object values are available where, for example I believe parameters and displays are not yet available in the Local Data, only in Init Code and later. The reason is, (or I might be wrong), that these values are inside the CPP class that is the object, and are declared in its constructor, or init() function or whatever. However, attributes are fine, since they literally do not exist as such in the CPP code, but are replaced with whatever value we select in the object.

#define LEMON(x,y)	x ## y
LogTextMessage("%d", LEMON(attr_gir, affe));

I am not sure what you’re doing here, but maybe you’ll have a clue now that we know any attributes are just plain and dumb replaced by the Java code generator, so the C compiler will never even see them. According to this logic, attr_gir does not exist, neither does affe. Java will not replace either with any valid value and they will be passed unchanged to the C compiler, which will stand in front of a mutilated animal carcass and won’t know what the meaning of it is.

1 Like

Thanks very much for this detailed reply. You’ve done some legwork that I should have done myself. Anyway, that’s quite a neat explanation. So, going back to my first example, before the preprocessor did anything with the macros, the patcher code generator basically turned

#define BANANA	attr_giraffe
LogTextMessage("%d", BANANA);

into

#define BANANA	4
LogTextMessage("%d", BANANA);

which is unproblematic. But it’s clear that similar substitutions would have been impossible in my other cases. I’d seen attributes replaced with numbers in xpatch.cpp before, but I didn’t know what I was looking at; I just assumed that any macros would been processed at an earlier stage.

I received different error messages than you, and I got compilation failures when the ‘define object’ was above the ‘LogTextMessage object’ (in fact I never tried it the other way). But I always had the giraffe attribute in the ‘LogTextMessage object’ (I should have been clearer about this), whereas I see from your example that you had it on the ‘define object’. This explains most of the difference. In my setup the patcher code generator never had a chance to replace the attribute name in the define with a number, because the attribute named was in a different object. So BANANA in the LogTextMessage code was replaced with “attr_giraffe” instead of “4”, and the CPP preprocessor (if I’ve got things right) interpreted this as the name of a non-existent variable. Hence “‘attr_giraffe’ was not declared in this scope”.

Sounds about right! Attributes have to be local to their respective object, otherwise when you place, say, 4 VCOs that each have a waveform attribute they’d be replacing each other’s attribute settings. Unless you make the code generator create unique hooks for each of these attributes, but then you lose the ability to address the attributes in your code in the first place?

I am not sure if this whole attribute replacement business is all foolproof, probably not. There probably is a much smarter way, like with the defines in a way you mention (or at least the code generator could go extracting and moving up all attributes / defines in the patch file or something)

But anyway, this is how Axoloti was written so this is what we have! It is a quite complex system juggling across 3+ programming languages, with a lot of patches as they kept on developing the ecosystem. Up to the point that they deciced to tear it all down and rewrite it into the V2 beta of Axoloti … which may be doing a lot differently. But then again it was only a beta so it was buggy and quite the showstopper.

By the way, if you want to reference something from one object in another one, there is the objref attribute. Check out the delay/read and write objects for example, or table/read and write etc.

By the way, if you want to reference something from one object in another one, there is the objref attribute. Check out the delay/read and write objects for example, or table/read and write etc.

I’ve delved fairly deeply into this topic. My object code is full of “parent->instance[object name]_i.[variable name]” stuff (something I copied from xpatch.cpp). This lets objects reference variables from other other objects without the need for objref attributes, which make objects too big for my liking. Unfortunately the parent->instance stuff is hard to remember and makes my code bulky without conveying much information. That’s mainly what I’m using macros for, to replace it with short, intelligible labels. Lately I’ve been getting greedy, and am trying to macrofy more and more of my code, but I think attributes have defeated me.

1 Like