Creating basic c++ bindings: object lifetimes and assigning 0 to LavHandles

8 views
Skip to first unread message

John

unread,
Jul 27, 2016, 9:32:51 AM7/27/16
to libaud...@camlorn.net

Hi,

 

The test scenario I’m using is the following:

·         A FileStreamer node connected to a hard limiter node

·         The hard limiter node is connected to the server

 

I’m trying to use RAII to manage object lifetimes. I’ve created a class that appropriately increments and decrements the ref count of a LavHandle, but I’m still seeing the file streamer node living longer than it should probably due to the following:

• Given some node a  and some other node b , the call a.connect(some_output, b, some_input) creates a strong reference from

b  to a . The connection remains alive until b  is collected or you explicitly kill it.

 

I was going to try disconnecting this node from the hard limiter if the handle’s ref count is 0, but the documentation cautions against using the function that returns the ref count of a handle. How else can I kill the node when all references to it are gone?

 

Also, is setting a LavHandle to 0 equivalent to an empty lav handle? E.g, it does nothing if 0 is passed to decreaseRefCount?

 

 

John

unread,
Jul 27, 2016, 10:56:12 AM7/27/16
to libaud...@camlorn.net

Tried looking for the implementation of the python bindings to see how it might be done, but can’t find the source files that expose this to python.

Austin Hicks

unread,
Jul 27, 2016, 3:51:15 PM7/27/16
to libaud...@camlorn.net

This is unfortunately a bit complicated, and you're probably not going to be able to treat it like Raii.  here is more information than you ever wanted; providing more information than you ever wanted is a thing I tend to do.


the short answer is do:

Lav_nodeSetIntProperty(node, Lav_NODE_STATE, Lav_NODESTATES_PAUSED; //silences and guarantees that it's not taking resources.
Lav_nodeIsolate(node); //Kills all connections in both directions.
Lav_handleDecRef(node); //Match the number of times you did Lav_handleIncRef

And then let Libaudioverse delete it whenever and be sure to write any callbacks to deal with the case of getting a handle that you think is deleted, because that can happen.

The long answer and justification for what probably seems like a stupid choice:

Libaudioverse is designed to interoperate with garbage collectors, which requires a lockfree deleter.  Unfortunately, Libaudioverse may be processing a block at the time that the GC wants to delete the object from its side.  This means that Lav_handleDecRef needs to never block and must postpone deletes until at least the next block.  Users of such languages expect connect to make strong references in one direction or another as well, and so I implemented it in the C++ portion of the library.  Doing this correctly on the side of a garbage collected language is hard.  WebAudio does something similar, but chooses the strong direction to be the opposite: nodes stay alive if they have inputs that are alive.  Which is better is debatable.  To understand the reason for this, consider: if neither direction is strong, you can't create a sound, 5 fixed filters, and a source without holding onto all 7 nodes, whereas the current approach only requires you to hold two.

Another seemingly unrelated factor is that I actually generate bindings and a large portion of the C++ code from Jinja2 templates and metadata in the metadata directory.  These scripts are very ugly and are on the to be cleaned up list, as I want others to be able to easily write bindings.  But the effect is that I can put more complicated code into them because I'm not writing it over and over or dealing with inconvenient things to type.  This has many other advantages such as adding new nodes to all bindings written using them without any human intervention (I haven't hand-written a node class. Ever).

As a consequence of all of this, the Python bindings use the proxy pattern.  There is a global dictionary of handles to states, and the things that the end user works with are actually proxies.  When the last proxy disappears, the external reference count goes to 0.  The state in the global dict stays around until such time as the deletion callback signals that that handle value will not be used again, allowing the bindings to resurrect an object which is identical for all intents.  Objects override equality to compare based off the value of the handle; the only way this can break is if you somehow use around 2 billion Libaudioverse objects and, if that becomes a problem, I'll go make it a 64-bit integer and be done.  This is the approach I intend to use for all official languages, and it may be what you want to try to do. 

I don't recall why I suggest not using Lav_handleGetRefCount at the moment, but there's probably a good reason.  Around line 12000 or so, i started using my own manual because I can't remember everything myself.  If this is something you absolutely need, I can go read it and figure out why i caution against it.  I think the issue is that it's not safe to use it in a scenario where multiple threads might be using Lav_handleDecRef (but Lav_handleDecRef is perfectly threadsafe).

the Python bindings are in bindings/python.  The three files there that are of interest to you are __init__.py.t, _lav.py.t, and _libaudioverse.py.t in the libaudioverse subdirectory.  Briefly, _libaudioverse.py.t produces ctypes bindings, _lav.py.t produces a layer on top of it that knows how to automatically return values and throw exceptions without dealing with pointers, and __init__.py.t is the bindings themselves.

The alternative to this admittedly complicated explanation is to make everyone who is in a garbage collected language call dispose on everything or suffer memory leaks.  Things like the Python with statement don't work for individual nodes because they exist for much longer than a short block of code.

In the mid-term, I intend to do C++ bindings that go via the official scripts and consider them at least somewhat high priority.  Certainly up there with getting Libaudioverse working on Mac.  At the moment, I'm blocking on getting the contributor copyright assignment agreement in place.  0.9 will come out immediately after that is done with.  I don't want to add anything else to the library that might be unstable until then, as I'm pretty confident that it's mostly bug-free at the moment (or,at least, the bugs should be minor).  On a sidenote, anything legal is frustrating, drawn-out, and boring all at the same time.

I hope this helps some.  If you choose not to adopt Libaudioverse and it's for something besides this, I want to hear about it.  One of the major reasons Libaudioverse hasn't moved forward quickly in a while is that no one is using it to the point where I can get feedback about which parts work well and which don't.  Admittedly this is my lack of promotion more than anything, but that will be changing shortly: 0.9 will be the first release I suggest that people use.
--
You received this message because you are subscribed to the Google Groups "libaudioverse" group.
To unsubscribe from this group and stop receiving emails from it, send an email to libaudiovers...@camlorn.net.
To post to this group, send email to libaud...@camlorn.net.
To view this discussion on the web visit https://groups.google.com/a/camlorn.net/d/msgid/libaudioverse/002401d1e817%240162f790%240428e6b0%24%40gmail.com.

Austin Hicks

unread,
Jul 27, 2016, 3:52:38 PM7/27/16
to libaud...@camlorn.net

O, yeah.  Forgot the last part.  I don't suggest passing 0 to Lav_handleDecRef.  It's kind of like passing null to delete and/or free, depending which you use.  I'm pretty sure that errors, but at the least it makes little logical sense to do it.


On 7/27/2016 10:56, John wrote:
--

John

unread,
Jul 28, 2016, 7:31:17 AM7/28/16
to libaud...@camlorn.net

Hi,

 

Thanks for the explanation, and yeah I couldn't figure out why you did it that way. I think it'd be helpful if you put this in the manual. I'm still very interested to try libaudioverse, because it looks much saner than the alternatives.

 

Could you check on the getRefCount function? getRefCount is the most convenient way to know when to pause and isolate.

 

Admittedly, I don't have much experience with audio libraries - using libsndfile, openal-soft and alure for .ogg files was not my idea of fun. So am unsure if these are the right semantics to use.

 

Why not have the strong reference go the other way i.e connecting A to B causes a strong reference from A to B? If you lose all references to nodes that have no inputs like file streamer, I can't think of why you'd want to keep them around.

 

I was asking about having a 0 LavHandle for being able to safely default construct nodes, or to leave the object in a state that the destructor can run when its moved. The alternative is a pointer to a lav handle, which adds another layer of indirection.

 

Glad to know that an autogenerated c++ binding is in the works. I'm writing the bare minimum to be able to use it for basic sound playback right now in a game.

John

unread,
Jul 29, 2016, 12:24:13 PM7/29/16
to libaud...@camlorn.net

Oh actually, the question I should be asking is: does a LavHandle returned by a function that creates some resource ever have the value 0?

 

From: Austin Hicks [mailto:cam...@camlorn.net]

Sent: Thursday, July 28, 2016 3:53 AM
To: libaud...@camlorn.net

Austin Hicks

unread,
Jul 29, 2016, 3:51:02 PM7/29/16
to libaud...@camlorn.net

A newly created handle can never have the value 0.  In that case, the first access flag is set (see Lav_handleGetAndClearFirstAccess--long name, but you don't use it often) and the refcount is put at 1.  I am 99.99% certain that the refcount is also set to 1 in all callback situations but am questioning that some now that we are having this conversation.  If that ends up not being the case, it's a bug.  If you use a HandleBox as described below, then it's not an issue for callbacks to have strong handles.


I'm looking at the implementation of Lav_handleGetRefCount, and using it won't crash you.  Unfortunately, it's not going to work out if you're trying to use it to ask if a handle has a refcount of 0, as there is no guarantee that the handle wasn't just deleted out from under you.  Handles with refcounts of 0 may also be reincremented if you're using callbacks.  It is also going to be hard to use it right if you have multiple threads changing the refcount, as the standard concerns apply.  Given that it's really bad practice to write your application that way if you're a C programmer, I'm not willing to extend my guarantees.  I hate refcounts like this too, but the alternatives are all equally unsatisfactory in other ways, so here we are.


Instead, consider doing what the Python bindings do and making a HandleBox class.  Ironically this is easier in C++.  The basic idea is this.  In the constructor, increment the refcount if the first access flag isn't set (if it is, then the refcount is 1 and no other HandleBox instances exist).  In the destructor, decrement the refcount.  And if you need to be able to default construct them, just check to see if the handle is 0 in both places and avoid doing anything until something sets it.  The final size of this class will likely be the same as an int, and you could probably inherit it if you wanted.  I will say that I do not at all like the idea of being able to default construct them, however.  bindings/python/libaudioverse/_lav.py.t has an example of how to implement this, plus a few tricky Python bits.  The generated bindings make it work ergonomically by spitting out wrappers that unwrap HandleBox instances automatically, so the rest of the code can just pretend it's a handle.

Keep in mind that move constructors are now a thing.  You could use them to avoid calls into Libaudioverse when moving the nodes around.  I'm assuming that you're using C++ because you need the performance, so it may be useful to add some.  You could also get some mileage in the default construction vs. pointer safety arena from std::shared_ptr, which will automatically clean up for you if and only if you ever filled it.

The ones I generate will indeed have a few levels of indirection.  There's really no way around it in the long run.  At some point I'm going to document the whole process of creating bindings.  The approach used for Python can work for any language, especially once I add a few more bits of information related to types to the metadata.  But my approach is admittedly heavyweight if you're doing it by hand.

The rationale for the strong references and the deletion logic and all that isn't in the manual because manuals aren't really the place for it.  The job of a manual is to tell you what you need to know to use it as efficiently as possible, not to pontificate on why choices were made.  There may be merit in a design rationale document that covers this stuff.  I'm also planning to blog some of it once I get 0.9 done and can do a release announcement.

The direction of strong references is actually more arbitrary than you would think.  Wanting to build a chain to the server and then let go of everything but the first node is actually a rare use case.  Normally, you're also holding stuff in the middle as well.  The most important bit of it is that things between nodes that you're forcing to stay alive via a strong reference be kept alive.

There are two unexpected behaviors that you can get.  Either things go unexpectedly silent because you accidentally let go of them (output->input is strong) or things keep playing even though you think they should be gone (input->output is strong).  But in either case you have to be aware of and deal with it.  WebAudio makes a strong case for output->input being strong by virtue of not letting you reuse most nodes you'd want to reuse, but I don't have that consideration because my API offers a reset and encourages you to cache as much as you possibly can.

That said, I'm not adverse to switching it at this point, though it would certainly be a big compatibility break.  If there's enough use cases for flipping it around, it might be worth it.  If I had known about how WebAudio chose to do it when I started the project, I probably would have done it the other way for consistency.  But looking like WebAudio is actually a complete coincidence, all be it one that says both good things about my design and WebAudio at the same time.

I've also toyed with making it configurable, but am not actually sure how to do that in anything resembling a sane manner internally.  We had a thread on here about it a while back, but no one had an opinion either way.  You're the first person to ask for it to be switched.  I'm somewhat indifferent, so I'll certainly entertain any discussion you want to have.

If you want me to explain the Python bindings architecture in full, I can do that.  Most of it is buried in this thread already in one place or another, though.

On the sanity point I fully agree.  In the future, I plan for Libaudioverse to make libsndfile.dll optional unless you need some additional and obscure file types.  It will also support static linking.  And of course Libaudioverse already lets you reliably turn HRTF on without hacking OpenALSoft.  Being able to reliably use a supported feature without patching the library it came from.  Imagine that.

John

unread,
Jul 30, 2016, 4:43:25 AM7/30/16
to libaud...@camlorn.net

Thanks, doing that worked perfectly.

 

I'm guessing that any discussion about changing the direction of strong references needs to be completed before V0.9 is released? I could get used to how it is now, if noone else has had any problem with it.

 

I'm actually only doing this audiogame in c++ as I was already somewhat familiar with it, availability of libraries and its more difficult to reverse engineer executables. Am strongly considering using a different language for future projects for productivity reasons. Not looking forward to serialization at all - why in the world doesn't c++ have any compile-time reflection yet? Visual Studio isn't very accessible, and CMake's syntax is ugly.

 

Though this is off topic, can anyone suggest any language alternatives that would work better and fits these requirements?

John

unread,
Jul 30, 2016, 1:55:23 PM7/30/16
to libaud...@camlorn.net

How much difference does connecting everything to the HardLimiter node before it goes to the server make compared to direct connections to the server?

 

I’m thinking of routing all sound effects and streaming music to separate nodes for volume adjustment purposes. Wouldn’t this sort of thing be a common use case? In which situations would you rather hold onto nodes in the middle instead of the starting node?

 

How’re you setting up the bindings? It looks like you aren’t using something like swig.

Austin Hicks

unread,
Aug 1, 2016, 1:22:34 PM8/1/16
to libaud...@camlorn.net

In the common case, you have buffer->source.  Here, you would hold both.


But say you have buffer->filter->source, and you know it's short-lived and you're going to abandon the objects.  In this case, you might hold the buffer and the source, or maybe just the source.  You can also drop incoming objects to the source with a .isolate(), so it's not like you need the filter node in order to be rid of the filter node.  And of course the buffer node goes away shortly after the filter drops out.  In cases like this, the direction of the reference doesn't matter much: you're holding both ends.

If you're doing stuff like instrument design, a use case which is not yet well supported, you'll have stuff like that but with way more filters that you never or rarely modify.  You'll have to hold only the output and any nodes you need to control to keep it alive, but don't need to necessarily hold the noise generators.  This will be even more true once Midi is supported, because I will definitely support linking Midi directly to properties.  I will also be adding nodes to implement mathematical transformations: for some languages, doing everything you possibly can inside Libaudioverse is the only way to do it in a reasonable manner.  Consequently, the midi control knob node (0 inputs and hypothetical for now) isn't something you'd need to hold either.

The bindings use a thing called pycparser to read the C header, infer information from function and parameter names which must follow a specific format, and then write a ctypes layer.  On top of that, they write a second layer that knows how to turn the functions into ones that throw exceptions and return values instead of ones that return errors and pass information out through pointers.  The third layer up is the public-facing components, generated by reading the files in the metadata directory with some manual implementation for the server and buffer classes.

The hard limiter makes sure that no values above 1 or below -1 make it out of Libaudioverse.  Without it, what happens in that case depends on the audio stack.  You mostly don't need it, if your usage is sane.  If users are allowed to do stuff that might lead to extremely large sample values,  it can be useful to make sure the sound cards don't do odd things.  It might be worth me putting this into audio_io when I rearchitect it; at that point, it's totally unnecessary to use it at all.  The two cases will have almost no performance difference: setting volumes and hard limiting and etc. are very fast.

We can discuss the direction of references after 0.9.  Libaudioverse is using semantic versioning, which means I'm allowed to do whatever I want before 1.0.  My goal is to not alienate people, so I'll be trying to keep the compatibility breakage to a minimum and provide notice.  But it's almost better to talk about it after 0.9, as we can get enough experiences to know if it would be worth it.  It's not a small change and doing it will probably introduce bugs.

One other possibility I've contemplated is an autoisolate that knows how to do things like make a buffer node go away when it's at the end or kill a filter that has no inputs and is outputting silence.

John

unread,
Aug 1, 2016, 1:27:41 PM8/1/16
to libaud...@camlorn.net

I did consider something like the autoisolate functionality you suggest – it might be the best way to solve the common cases. Lets revisit this post 0.9.

Reply all
Reply to author
Forward
0 new messages