3

I am working on a Java library that is a thin wrapper for the Windows Waveform Functions to play 24 bit audio through Java. (The JVM only supports 8bit and 16bit audio).

The paradigm in the Windows Waveform Functions is:

  1. Create Header struct
  2. call waveOutPrepareHeader on the Header.
  3. Send header to sound card
  4. Sound card plays asynchronously (which means the Header must stay in memory for the duration of the audio playing)
  5. When the sound card is done playing, it sets a "Done" bit in the header
  6. When the "done" bit is set, I have to call waveOutUnprepareHeader
  7. Then I can remove the header from memory

Given that my Java library is going to be a thin wrapper for the native Waveform Functions, I have a class for the Header Pointer, so I can keep it in scope for as long as needed, pass it around as needed, and eventually call waveOutUnprepareHeader on it.

public class WaveHeader {
  long waveHeaderPointer;
  
  public WaveHeader(byte[] buffer) {
    waveHeaderPointer = HWaveOut.createHeader(buffer, buffer.length);
  }
}

The native code being called above on line 5 (HWaveOut.createHeader()) is:

JNIEXPORT jlong JNICALL Java_net_joshuad_waveformjni_HWaveOut_createHeader
(JNIEnv * env, jclass jclass, jbyteArray jbuffer, jint jBufferSize) {
    char* buffer = new char[jBufferSize];
    asCharArray(env, jbuffer, buffer);
    WAVEHDR* headerOut = new WAVEHDR{ buffer, (DWORD)jBufferSize, 0, 0, 0, 0, 0, 0 };
    std::cout << "[C++] Header out location: " << headerOut << std::endl;
    return (jlong)headerOut;
}

As you can see, I allocate a WAVEHDR on the heap in C++.

It is my understanding that I am responsible for de-allocating the WAVEHDR when I am done with it -- that the Java Garbage Collector won't destroy it for me.

I initially considered putting the de-allocation code in finalize() in java, so that the C++ struct is always automatically de-allocated when the java object is garbage-collected in java, but according to this answer this method will cause memory leaks.

I then thought of using the compiler warnings for unclosed resources in the classes like InputStream to catch any mistakes I make, but even if I make WaveHeader Closable, I don't get the compiler warnings I'm used to if I don't call close().

Is there a good way to protect myself from accidental memory leaks here?

Grumblesaurus
  • 3,021
  • 3
  • 31
  • 61
  • 1
    *It is my understanding that I am responsible for de-allocating the WAVEHDR when I am done with it* -- That's the case regardless of whether you are using Java or not. How do you handle this if this were purely a C++ application? – PaulMcKenzie Sep 08 '20 at 03:02
  • I have a C++ solution which works appropriately. I can write that exact solution in Java and I'd be fine for my immediate design. I am asking if there's some mechanism in Java I can use to help avoid programmer mistakes, because it's not intuitive for a Java programmer to remember that they have to responsibly de-allocate resources. – Grumblesaurus Sep 08 '20 at 04:32
  • 1
    Without a proper cleanup function in Java, then you're stuck in creating and keeping track of the allocated memory in the C++ code. Also if you've clearly documented how to use the functions, that's the programmer's fault by not following the documentation. The Java programmer has to realize they are making calls to a non garbage-collected language, where that language is dynamically creating objects, and deletion is under that language's control, not Java. – PaulMcKenzie Sep 08 '20 at 15:15

2 Answers2

1

One solution is to create a pool of these WAVEHDR objects at startup and only allow the Java code to take objects from the pool and recycle them. Failure to return objects will result in an empty pool right after startup and a crash.

Botje
  • 26,269
  • 3
  • 31
  • 41
1

You are right, the compiler won't warn you about missing close(), but lint or similar static code analysis tool, will. At any rate, Closeable is the recommended way to go, and if you use it with try(), the language will be on your side. Still, it's a good practice to call close() from finalize() (unless you know that your JVM has the bug described by Steven M. Cherry).

By the way, he did not say that finalize() caused a memory leak; this was a heap corruption, something much worse; but this report is of 2008, so you have little chance to encounter this bug in production.

As for the specific case of WAVEHDR, I would suggest not to allocate it in C++ on heap, but rather to keep it all (with the buffer) allocated in Java as a direct ByteBuffer:

public class WaveHeader {
  private ByteBuffer waveHeader;
  private final static int PTR_LENGTH = 8; // 64-bit Windows
  private final static int DWORD_LENGTH = 4;
  private final static int WAVEHDR_LENGTH = 4*PTR_LENGTH + 4*DWORD_LENGTH;
  
  private native static void init(ByteBuffer waveHeader);

  public WaveHeader(int bufferLength) {
    waveHeader = allocateDirect(bufferLength + WAVEHDR_LENGTH);
    init(waveHeader);
  }
}
JNIEXPORT void JNICALL Java_net_joshuad_waveformjni_WaveHeader_init(JNIEnv* env, jclass clazz, jobject byteBuffer) {
    auto waveHeader = reinterpret_cast<WAVEHDR*>(env->GetDirectBufferAddress(byteBuffer));
    jlong capacity = env->GetDirectBufferCapacity(byteBuffer);
    waveHeader->lpData = reinterpret_cast<LPSTR>(waveHeader+1);
    waveHeader->dwBufferLength = capacity-sizeof(WAVEHDR);
}

Now you don't care about close() or managing the memory of your C++ object: it is all managed by Java.

Alex Cohn
  • 56,089
  • 9
  • 113
  • 307