more panama notes - ffmpeg
Michael Zucchi
notzed at gmail.com
Mon Feb 3 03:45:49 UTC 2020
Hi guys,
As a bit of a weekend project i started prototyping work on porting
jjmpeg to panama. jjmpeg is a 'lean' binding that only binds what it
needs to, hides most of the messy details and tries to present a 'sane'
interface. As FFmpeg wasn't really designed as a 'public' api there is
a lot that isn't worth exposing or just shouldn't be. This vastly
reduces the api size by ignoring all the stuff that isn't needed as a
user of the library although it also means important parts that I don't
use may not be available.
I think i've mentioned before that there are now other alternatives for
reading/writing video for java but when I started it there was really
just 'xuggler' as a cross-platform solution: which was gigantic, hard to
compile, miserable to use. Things like media players aren't any use for
it's use-case which is accessing the pixel bits and not for playing movies.
In this iteration for panama i'm using an application-specific generator
which directly creates the public classes so there is no extra glue
required to call some low-level interface. It is controlled by a
meta-data file. Based on that it is mostly automated with a sprinkling
of hand-rolled cases where the generator isn't sufficient or not worth
extending (it's messy enough). I've managed to avoid the need for a
Pointer<> or Array<> type so far by designing around those cases. Most
of the complexity is with the setup/config stage which isn't performance
critical - at the end of the day you just get a buffer of pixel or sound
samples and that will just be a bytebuffer or memorysegment and that's
the only time performance matters.
I don't use jextract mostly because I want far more control in what gets
created, it also doesn't work yet[1].
It's still work in progress, maybe 60% coverage of prior versions. It's
mostly been straightforward but here's some notes about the story so
far. The main panama specific issue is with ByteBuffer vs <T extends
Buffer>, and maybe varargs upcalls.
unsafe
I just have to use foriegnunsafe /everywhere/. Some values created
need to be freeable by libav*, so you need to use av_malloc() for
all allocations, and that generally requires unsafe to be able to
initialise them.
When you access the pixel or sample data it is via FFmpeg managed
memory buffers, so unsafe is required there.
<T extends Buffer> vs ByteBuffer
In JNI any type of direct buffer (ByteBuffer, IntBuffer, etc) can be
accessed using GetDirectBufferAddress and GetDirectBufferCapacity.
But the only possible way to pass a buffer address to C with panama
is with a ByteBuffer via MemorySegment.of*().
jjmpeg has an implementation of the JavaFX PixelReader[2] (and
Writer) interface which allows one to directly write libswscale
processed data into a WritableImage. The problem is the read pixels
call takes a <T extends Buffer> and not a ByteBuffer.
In practice - until openjfx 13 - it always uses the Buffer method
and that is always passed a ByteBuffer. But openjfx adds changes
that allow for an IntBuffer to be used instead. A workaround for
JavaFX 13+ is just to ensure the WritableImage is backed by the
correct type, amongst other requirements.
I don't know of any other similar cases in the jdk but i haven't looked.
vector access
libav* exposes various arrays as a length + pointer to pointers.
Since this is external memory it needs to be wrapped in an unsafe
segment before dereferencing the pointers elements, every time. At
least with the current design.
typedef struct AVFormatContext { ... unsigned int nb_streams;
AVStream **streams; ... }
Becomes:
public final static VarHandle field$nb_streams = MemoryHandles.withOffset(i32Handle, 44);
public final static VarHandle field$streams = MemoryHandles.withOffset(addrHandle, 48);
public AVStream getStream(int index) {
int length = (int)field$nb_streams.get(addr());
if (Integer.compareUnsigned(index, length) >= 0) { throw new ArrayIndexOutOfBoundsException(); }
MemoryAddress base = Memory.ofNative((MemoryAddress)field$streams.get(addr()), length * 8).baseAddress();
return resolve((MemoryAddress)addrVHandle.get(base, (long)index), AVStream::new);
}
This is absolutely not a performance critical path so who cares to
be honest. Memory.ofNative() just calls
ForiegnUnsafe.ofNativeUnchecked().
sentinel-terminated lists
In a couple of places the api exposes arrays which aren't defined by
a length but by a terminating sentinel value. Since this is
external memory the process to determine the length requires an
extra step:
MemoryAddress list = Memory.ofNative(data,
Integer.MAX_VALUE).baseAddress(); int len = 0; while
(read_value(list, len) != sentinel) len++; allocate array copy len
elements
I guess the api could expose an iterator or stream but this adds
temporal issues for non-const data.
For strings specifically I now just call libc:strlen() which makes
the code quite simple:
String str = new String(Memory.ofNative(cstr,
strlen(cstr)).toByteArray())
structure fields
I generate sparse MemoryLayouts that only cover the items i'm using
by just padding over the private parts. Actually for now I
generally don't use the layout and just use the offsets from the c
compiler to directly instantiate the variable handles, but they're
there incase I change my mind or find another use for them. They
are needed for FunctionDescriptions for down calls which take
pass-by-value structs but that isn't common.
Many of these fields may be able to be removed entirely by using
some generic "get/set" calls from libavutil/opts.h, but I need to
delve further.
These rest are just informational, these aren't things any sane library
does internally let alone exposes it.
AVDictionary
This is just a Map<String,String> but implemented so poorly it
beggars belief (I think it's legacy from the libav split).
Internally it's an array of pointer pairs with a length. But /every
time you change it's key set/, the pointer array is reallocated and
copied. This requires the calls to take an AVDictionary **. It
also has a bizarrely complicated insert and query interfaces that
let you control all sorts of things like treating the arguments as
constants to be copied, or allocated memory to steal. The array and
all strings must be allocated using av_malloc() as it may freely
call av_free() whenever it wants.
Solution here is what i did with jjmpeg/jni - the Java class is just
a plain TreeMap<> and I have code which converts between that and
the (private) C structure.
av_log_set_callback
void av_log_set_callback(void (*callback)(void*, int, const char*,
va_list));
Rather than a formatted string the callback gets the parameters for
vprintf etc. The va_list, which gets turned into (I presume) some
platform specific struct.
(jextract output on just the function above)
private static final FunctionDescriptor
av_log_set_callback$callback$DESC = FunctionDescriptor.ofVoid(false,
MemoryLayouts.SysV.C_POINTER, MemoryLayouts.SysV.C_INT,
MemoryLayouts.SysV.C_POINTER, MemoryLayout.ofSequence(1,
MemoryLayout.ofStruct(
MemoryLayouts.SysV.C_INT.withName("gp_offset"),
MemoryLayouts.SysV.C_INT.withName("fp_offset"),
MemoryLayouts.SysV.C_POINTER.withName("overflow_arg_area"),
MemoryLayouts.SysV.C_POINTER.withName("reg_save_area")
).withName("__va_list_tag")) );
In an earlier jjmpeg i just formatted the string before passing it
back to Java. I don't see any sane way to handle it here.
opt.h
This is another way to access members and provides generic
getter/setters. But it's quite complex and the api isn't great and
one has to delve into the source of each 'object' to determine what
the symbolic name is for every field as they might not match. But
it might be able to be used to remove most of the structure peeking
and poking, at some size+perf cost.
Cheers,
Z
[2]
https://openjfx.io/javadoc/13/javafx.graphics/javafx/scene/image/PixelReader.html
[1]
notzed at shitzone:~/src/panamaz/test-ffmpeg$ cat ffmpeg-api.h #include
<libavformat/avformat.h> #include <libavcodec/avcodec.h> #include
<libswscale/swscale.h> #include <libavutil/imgutils.h>
notzed at shitzone:~/src/panamaz/test-ffmpeg$
/opt/panama-jextract/bin/jextract -I /usr/lib64/clang/9.0.1/include
ffmpeg-api.h WARNING: Using incubator modules: jdk.incubator.foreign,
jdk.incubator.jextract Exception in thread "main"
java.util.ConcurrentModificationException at
java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1226) at
jdk.incubator.jextract/jdk.internal.jextract.impl.TreeMaker.checkCache(TreeMaker.java:57)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.TreeMaker.createVar(TreeMaker.java:222)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.TreeMaker.createTree(TreeMaker.java:82)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.TreeMaker.createFunction(TreeMaker.java:142)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.TreeMaker.createTree(TreeMaker.java:84)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.Parser.lambda$parse$2(Parser.java:91)
at
java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1624)
at
java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.Parser.parse(Parser.java:72)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.JextractTaskImpl.parse(JextractTaskImpl.java:59)
at
jdk.incubator.jextract/jdk.internal.jextract.impl.JextractTaskImpl.parse(JextractTaskImpl.java:53)
at
jdk.incubator.jextract/jdk.incubator.jextract.tool.Main.run(Main.java:187)
at
jdk.incubator.jextract/jdk.incubator.jextract.tool.Main.main(Main.java:97)
More information about the panama-dev
mailing list