clojure

flowthing 2026-05-15T08:21:17.167469Z

I'm working with https://github.com/carlanton/m3u8-parser, which apparently uses something called https://immutables.github.io/ to generate code. How do I interop with that generated code from Clojure? I cannot figure out a way to call methods that Immutables has generated. For example:

λ clj -Srepro -Sdeps '{:deps {io.lindstrom/m3u8-parser {:mvn/version "0.30"}}}' -M -r
Clojure 1.12.5
user=> (import '(io.lindstrom.m3u8.model MediaSegment))
io.lindstrom.m3u8.model.MediaSegment
user=> (-> (MediaSegment/builder) (.uri ""))
Execution error (IllegalArgumentException) at user/eval136 (REPL:1).
No matching method uri found taking 1 args for class io.lindstrom.m3u8.model.MediaSegment$Builder

flowthing 2026-05-15T08:23:10.824789Z

Apparently the .uri method lives in a generated class called MediaSegmentBuilder, whereas MediaSegment/builder returns a MediaSegment$Builder , which extends MediaSegmentBuilder .

😂 4
reefersleep 2026-05-15T08:26:00.884129Z

With pain, it seems.

🤣 1
reefersleep 2026-05-15T08:27:26.266969Z

Trying to figure out a class hierarchy to eke out the exact puzzle pieces and how they fit together to allow me to make the 1 function call I need is my least favourite part of Java interop.

flowthing 2026-05-15T08:29:50.784369Z

I even tried using reflection to generate an instance of MediaSegmentBuilder instead of MediaSegment$Builder and then go through that, but:

user=> (def constructor (first (seq (.getDeclaredConstructors MediaSegmentBuilder))))
#'user/constructor
user=> (.setAccessible constructor true)
nil
user=> (def segment (.newInstance constructor (into-array Object [])))
Execution error (UnsupportedOperationException) at io.lindstrom.m3u8.model.MediaSegmentBuilder/ (MediaSegmentBuilder.java:48).
Use: new MediaSegment.Builder()

flowthing 2026-05-15T08:31:17.935659Z

With pain, it seems.I'll say. I am this close to just writing a Clojure M3U8 parser from scratch to sidestep this insanity. 😛 It would probably be less work than making this one method call work. (Not blaming Clojure, just to be clear, but rather the OOP madness that makes people use things like Immutables.)

reefersleep 2026-05-15T08:34:37.857769Z

indeed

reefersleep 2026-05-15T08:36:45.112359Z

The builder pattern normally (I think? long ago) has a build() or similar fn that actually returns the object you're trying to build. Usually after you call various fns to set up the desired state of the thing you're trying to build. Can you use whatever it produces?

flowthing 2026-05-15T08:37:29.996819Z

I tried that, but I cannot, because:

user=> (-> (MediaSegment/builder) (.build))
Execution error (IllegalStateException) at io.lindstrom.m3u8.model.MediaSegmentBuilder/build (MediaSegmentBuilder.java:393).
Cannot build MediaSegment, some of required attributes are not set [duration, uri]

reefersleep 2026-05-15T08:37:42.927559Z

hahaha amazing

flowthing 2026-05-15T08:37:57.373799Z

That's one word for it. 😛

reefersleep 2026-05-15T08:38:00.324379Z

well does it have something like setUri() instead of uri()

reefersleep 2026-05-15T08:38:01.547409Z

?

reefersleep 2026-05-15T08:38:12.430659Z

or other "setter" methods

flowthing 2026-05-15T08:40:05.452249Z

No... MediaSegment$Builder has almost nothing:

user=> (clojure.pprint/pprint (reflect/reflect MediaSegment$Builder))
{:bases #{io.lindstrom.m3u8.model.MediaSegmentBuilder},
 :flags #{:public},
 :members
 #{{:name build,
    :return-type io.lindstrom.m3u8.model.MediaSegment,
    :declaring-class io.lindstrom.m3u8.model.MediaSegment$Builder,
    :parameter-types [],
    :exception-types [],
    :flags #{:public :bridge :synthetic}}
   {:name io.lindstrom.m3u8.model.MediaSegment$Builder,
    :declaring-class io.lindstrom.m3u8.model.MediaSegment$Builder,
    :parameter-types [],
    :exception-types [],
    :flags #{:public}}}}
nil
MediaSegmentBuilder has more:
user=> (clojure.pprint/pprint (->> (reflect/reflect MediaSegmentBuilder) :members (map :name)))
(access$800
 duration
 access$1100
 access$900
 byteRange
 OPT_BIT_CUE_IN
 gap
 dateRange
 INIT_BIT_URI
 dateRange
 access$100
 segmentMap
 access$200
 access$1400
 bitrate
 cueIn
 uri
 duration
 uri
 access$500
 segmentMap
 addSegmentKeys
 segmentKeys
 from
 formatRequiredAttributesMessage
 gap
 addPartialSegments
 partialSegments
 cueIn
 access$400
 access$1700
 programDateTime
 access$1300
 access$1000
 access$600
 access$1200
 gapIsSet
 cueInIsSet
 addAllSegmentKeys
 build
 dateRange
 OPT_BIT_DISCONTINUITY
 programDateTime
 addPartialSegments
 discontinuity
 access$300
 addSegmentKeys
 optBits
 byteRange
 bitrate
 createSafeList
 bitrate
 title
 segmentMap
 cueOut
 access$1800
 programDateTime
 INIT_BIT_DURATION
 io.lindstrom.m3u8.model.MediaSegmentBuilder
 discontinuity
 title
 addAllPartialSegments
 createUnmodifiableList
 partialSegments
 segmentKeys
 cueOut
 cueOut
 title
 access$1600
 OPT_BIT_GAP
 initBits
 access$1500
 access$700
 byteRange
 discontinuityIsSet)

reefersleep 2026-05-15T08:43:13.731899Z

Yeah ok. I don't get it. If class B extends A, shouldn't the methods of B be those of A + those specific to B?

reefersleep 2026-05-15T08:43:37.680429Z

Does it take more than 1 argument?

reefersleep 2026-05-15T08:43:53.588119Z

No matching method uri found taking 1 args for class io.lindstrom.m3u8.model.MediaSegment$Builder

reefersleep 2026-05-15T08:44:00.788559Z

or less 😅

exitsandman 2026-05-15T08:52:03.317089Z

is the class module-private? no

exitsandman 2026-05-15T08:54:59.798069Z

(def -no-reflection-warning ^[String] io.lindstrom.m3u8.model.MediaSegmentBuilder/.uri)
(def -reflection-warning io.lindstrom.m3u8.model.MediaSegmentBuilder/.randomNonexistentMethod)

(try (-no-reflection-warning (MediaSegment/builder) "foo")
  (catch IllegalAccessError e e))
exception message:
"failed to access class io.lindstrom.m3u8.model.MediaSegmentBuilder from class repl$invoke__MediaSegmentBuilder__DOT_uri__10381 (io.lindstrom.m3u8.model.MediaSegmentBuilder is in unnamed module of loader 'app'; repl$invoke__MediaSegmentBuilder__DOT_uri__10381 is in unnamed module of loader clojure.lang.DynamicClassLoader @759073b4)"

flowthing 2026-05-15T09:17:35.951699Z

> I don't get it. If class B extends A, shouldn't the methods of B be those of A + those specific to B? My thoughts exactly.

flowthing 2026-05-15T09:20:19.260859Z

Oh. Is .uri private?

exitsandman 2026-05-15T09:20:39.798309Z

no, it's public

exitsandman 2026-05-15T09:22:02.169309Z

however:

(java.lang.reflect.Modifier/isPublic (.getModifiers MediaSegmentBuilder)) ; => false

1
flowthing 2026-05-15T09:22:02.541549Z

Oh, right, I misread somehow.

{:name uri,
 :return-type io.lindstrom.m3u8.model.MediaSegment$Builder,
 :declaring-class io.lindstrom.m3u8.model.MediaSegmentBuilder,
 :parameter-types [java.lang.String],
 :exception-types [],
 :flags #{:public :final}}

exitsandman 2026-05-15T09:22:43.320699Z

I wonder if the issue is ..$Builder inhering from ..Builder

exitsandman 2026-05-15T09:25:25.135669Z

and note

exitsandman 2026-05-15T09:25:49.377939Z

(let [-subinterface-accessor ^[String] io.lindstrom.m3u8.model.MediaSegment$Builder/.uri]
  (try (-subinterface-accessor (MediaSegment/builder) "foo")
    (catch IllegalAccessError e (re-find #"MediaSegment$Builder" (ex-message e)))))
; => nil
Clojure tries calling through the (inaccessible) method of the superinterface. Perhaps this is done because Clojure assumes the opposite is true, i.e. hidden class inherits from exposed interface.

flowthing 2026-05-15T09:28:54.225969Z

Hmm, this seems to work:

user=> (defn invoke-private [obj class method-name param-types & args]
  (let [m (doto
            (.getDeclaredMethod class method-name (into-array Class param-types))
            (.setAccessible true))]
    (.invoke m obj (object-array args))))
#'user/invoke-private
user=> (invoke-private (MediaSegment/builder) MediaSegmentBuilder "uri" [String] "")
#object[io.lindstrom.m3u8.model.MediaSegment$Builder 0x1851c7d2 "io.lindstrom.m3u8.model.MediaSegment$Builder@1851c7d2"]

exitsandman 2026-05-15T09:29:27.795999Z

yes, "cheating" with setAccessible always works xD

flowthing 2026-05-15T09:30:47.949729Z

My goodness. Well, this is awful, and the performance is probably terrible, but at least it works.

reefersleep 2026-05-15T09:31:21.833319Z

Victory... but at what cost

2
flowthing 2026-05-15T09:31:22.266499Z

In any case, thanks for https://clojurians.slack.com/archives/C03S1KBA2/p1778836922169309?thread_ts=1778833277.167469&cid=C03S1KBA2! That was the key to the puzzle.

exitsandman 2026-05-15T09:31:36.317399Z

eh, if you grab the method at compile time it should be fast enough

flowthing 2026-05-15T09:31:45.851259Z

Oh, that's true.

exitsandman 2026-05-15T09:32:25.093739Z

Fwiw it works just fine in Java

flowthing 2026-05-15T09:33:31.785209Z

What works just fine in Java?

exitsandman 2026-05-15T09:33:45.608539Z

the lib

flowthing 2026-05-15T09:33:53.107159Z

Oh, yes, no doubt.

exitsandman 2026-05-15T09:34:57.048169Z

perhaps your best bet is a Java shim then

flowthing 2026-05-15T09:35:44.515319Z

Yeah, I'll consider that if this project goes further than the first spike I'm working on now. 👍

p-himik 2026-05-15T10:10:26.585479Z

It seems that there's no Ask post for it. There's https://ask.clojure.org/index.php/4255/cannot-resolve-public-generic-method-package-private-class but it's slightly different - it's specifically about generic methods. But maybe it's enough to mention your issue there and vote for it.

flowthing 2026-05-15T10:11:01.697329Z

Thanks, will do!

flowthing 2026-05-15T10:12:23.220499Z

At a quick glance, the last comment in that issue looks similar to my issue.

flowthing 2026-05-15T10:13:02.892179Z

Oh, but it's not.

exitsandman 2026-05-15T10:32:46.162949Z

E: remove useless code (E: https://pastebin.com/12935dDX) The reason for this working in java and not in the AFn appears to be that Java calls into the subinterface's method which while being invisible to reflection does exist at the bytecode level, whereas Clojure only sees the superinterface's package-internal method and either refuses to emit the call (direct method call case) or eats an AccessError at runtime (method-as-value case). What I find remarkable here is that Java itself allows public classes to inherit from module-private ones, and handles the accessibility only at the level of bytecode, presenting a different pictore to the reflection machinery! Any tool relying on reflection and not barging in with setAccessible would choke here.

👀 2
exitsandman 2026-05-15T11:05:04.400709Z

as per method handles...

(defn -find-that-method [cls]
  (.findVirtual (MethodHandles/lookup)
    cls "uri"
    (MethodType/methodType cls (into-array [String]))))

(defn -invoke-handle [h & args]
  (java.lang.invoke.MethodHandle/.invokeWithArguments h (object-array args)))

(-invoke-handle (-find-that-method MediaSegment$Builder) (MediaSegment/builder) "foo") ; ok

(try
  (-find-that-method MediaSegmentBuilder)
  (catch IllegalAccessException _)) ; access error

seancorfield 2026-05-15T21:42:41.481889Z

We all know (catch Throwable ..) is "bad" -- are there any really useful situations where you might actually want to catch an Error rather than just Exception? Looking over Error and its known subclasses in the Java 25 docs, AssertionError and maybe IOError (not IOException) seem like the only possible cases that might warrant being caught -- but folks often talk about turning assert off in production, which would eliminate the former, and I don't know of any real world case of the latter... Thoughts?

Steven Lombardi 2026-05-17T00:29:56.288549Z

I've had to catch throwable once or twice. Can't remember the specifics but if you do it and end up just throwing again, make sure you reset the interrupted flag in the interrupted case.

p-himik 2026-05-15T21:46:04.260429Z

> I don't know of any real world case of the latter... What should such a case look like to be useful to you? Because there are a lot of instances of catching/throwing IOError out there, even in the JDK itself.

seancorfield 2026-05-15T21:47:48.689419Z

IOError or IOException?

p-himik 2026-05-15T21:48:00.128879Z

Both.

seancorfield 2026-05-15T21:48:27.833079Z

Java's docs specifically say that Error types generally should not be caught...

p-himik 2026-05-15T21:50:31.703699Z

🤷 From .jline.reader.impl.LineReaderImpl#readLine:

catch (IOError e) {
            if (e.getCause() instanceof InterruptedIOException) {
                throw new UserInterruptException(buf.toString());
            } else {
                throw e;
            }
        }
.jline.reader.impl.LineReaderImpl#doSearchHistory:
catch (IOError e) {
            // Ignore Ctrl+C interrupts and just exit the loop
            if (!(e.getCause() instanceof InterruptedException)) {
                throw e;
            }
            return true;
        }
jdk.internal.loader.Resource#getURL:
public URL getURL() {
                    try {
                        return file.toUri().toURL();
                    } catch (IOException | IOError e) {
                        return null;
                    }
                }
And so on.

seancorfield 2026-05-15T21:51:00.970319Z

I just scanned our New Relic logs and I see ~70 IOException caught in the last month and zero IOError... Hmm, interesting... thanks.

p-himik 2026-05-15T21:51:46.772479Z

I love the doc of IOError: "Thrown when a serious I/O error has occurred." :D

1
seancorfield 2026-05-15T21:52:25.569109Z

Given that Java docs say IOError is for "serious" I/O errors, I wonder what in the JDK is wrapping Exception types in IOError and throwing that? 🤯

p-himik 2026-05-15T21:53:27.579529Z

Another fun one - an exception caused an error:

catch (IOError e) {
                    throw (IOException) e.getCause();
                }

p-himik 2026-05-15T21:55:56.358919Z

jdk.internal.io.JdkConsoleImpl wraps IOException into IOError in multiple places. And a bunch of other classes, this is a very common pattern in JDK:

catch (IOException e) {
            throw new IOError(e);
        }

seancorfield 2026-05-15T21:55:56.907999Z

cloning openjdk source so I can search it locally ... damn, that thing's big!

😄 2
p-himik 2026-05-15T21:56:38.904089Z

.jline.utils.Curses#tputs wraps any exception:

catch (Exception e) {
            throw new IOError(e);
        }

p-himik 2026-05-15T21:58:23.477359Z

> cloning openjdk source so I can search it locally ... damn, that thing's big! --depth 1 ;) Also, this pings back to the old discussion we had where you said you don't really need to integrate your Clojure projects with Java sources since you can browse those separately. In my case, I don't even need to clone anything. :D I love the outcome so far, but I hate the fact that I'm hooked onto IntellIJ IDEA with no prospects of being able to drop it.

seancorfield 2026-05-15T21:59:17.929349Z

Bizarre. Since those are all internal packages, I wonder if that wrapping'n'catching is also purely internal and those IOError never escape into user code?

seancorfield 2026-05-15T22:01:41.600339Z

Delightful...

} catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (IOError error) {
            Throwable cause = error.getCause();
            if (cause instanceof IOException e) {
                throw new UncheckedIOException(e);
            }
            throw new RuntimeException(cause);
        }
🤯

seancorfield 2026-05-15T22:04:47.574849Z

I'll dig into this further on Monday, since this is related to some code cleanup I'm doing at work. Food for thought. Thanks @p-himik!

👍 1
p-himik 2026-05-15T22:13:55.920059Z

> I wonder if that wrapping'n'catching is also purely internal and those IOError never escape into user code? It can escape from .JdkConsoleImpl#readPassword, via java.lang.System#console.

seancorfield 2026-05-15T22:17:39.191099Z

Yeah, the Console class has methods that throw IOError -- but most of those so far seem to be interactive stuff, rather than anything that would be used inside a web app? I searched for IOError in the JDK source and there are a lot of places to look at (on Monday).

p-himik 2026-05-15T22:21:27.937209Z

Heh, sun.nio.ch.Poller has it in its static initializer. But I guess "everything blows up when the process starts" is also not that interesting of a use case.

2026-05-15T23:27:16.933209Z

netty throws a io.netty.util.internal.OutOfDirectMemoryError for direct memory exhaustion during serialisation, which it can be useful to wrap to locate which class it it is trying to serialise.