Fork me on GitHub
#clojure
<
2022-03-26
>
ahungry01:03:44

I think the issue crinklywrappr was suggesting as a potential issue is a fractured/split eco-system - imagine if some lib authors for non-CLI packages begin relying on deps.edn gitlibs and only make their packages available in a similar manner - now those who have historically relied on maven/clojars/lein may not have an easy avenue to participating in said lib's usage without going through their own hoops to make accessible?

crinklywrappr02:03:00

A coworker approached this from another angle I hadn't thought of. What if you want to publish your library to clojars, but need a dependency which isn't in a maven repo? Do you give up or fork the dep and pray you don't have to go further down the stack? It's not hard to see how this encourages movement away from maven.

seancorfield02:03:56

> What if you want to publish your library to clojars, but need a dependency which isn't in a maven repo? This is already true in the Java world if you s/clojars/maven central/

seancorfield02:03:03

The current solution is usually to bring the dependency directly into your project as shaded source code.

seancorfield02:03:11

(in both Java and Clojure worlds)

crinklywrappr02:03:03

Thanks Sean. I didn't know that.

seancorfield03:03:47

Also, Leiningen has at least one plugin that has supported git dependencies (separate from Maven-style deps) for many years: https://github.com/tobyhede/lein-git-deps

šŸ‘ 1
crinklywrappr06:03:14

That puts me a little at ease

phronmophobic01:03:01

While I understand the reasoning for deps not looking inside jar files from maven for git deps, I'm not sure it would be a bad idea for that type of functionality to exist. Putting libraries as jars on clojars has other benefits besides just making the library available to leiningen users: ā€¢ clojars maintains a list of available libraries and versions ā€¢ clojars maintains stats on downloads which can be a useful metric for evaluating libraries ā€¢ packaging your dep in a jar allows you to choose which files belong or don't belong in the shipped library ā€¢ procuring a packaged jar allows you to avoid downloading unnecessary files ā€¢ clojars integrates with other infrastructure like cljdoc

Cora (she/her)01:03:45

I definitely use number of downloads to get an idea of popularity, and not just in clojars but npm and rubygems, too

seancorfield02:03:33

Given that the CLI defers to Maven libraries to deal with the whole JAR/pom/dependencies thing, I suspect it would be non-trivial to additionally pull deps.edn from a downloaded JAR file and then re-run dependency analysis and fetching on that... and what if the dependency versions in pom.xml and deps.edn differ? That's probably the case for some of the Contrib libraries since pom.xml is the "system of record" for those, since they are tested/built via mvn. Some of them have project.clj files that are outdated since those were added as a "developer convenience" for the maintainer -- and the same is true of any Contrib libs that have deps.edn (as well as pom.xml).

seancorfield02:03:54

(This discussion should probably move to #tools-deps since going deeper really belongs there)

mars0i04:03:05

I'm suspect that someone's seen something like this and can point me in the right direction. I'm using a Java class, Foodspot that I defined with deftype. (I need it in order to pass it to a Java class in a library.) I'm getting this error: "forage.mason.foodspot.Foodspot cannot be cast to forage.mason.foodspot.Foodspot". Huh?

mars0i04:03:30

Specifically I'm passing a Foodspot to a Clojure function. Ah, and when I remove the type hint ^Foodspot from the argument to the function, the error goes away. But that's still puzzling. And it's worked with the type hint in other cases.

hiredman04:03:27

Java classes are not entirely identified by name

hiredman04:03:54

They are identified by name and, uh, I guess defining class loader, so you can have multiple classes with the same name that are not the same, which results in errors like that

hiredman04:03:31

And every time you evaluate a given deftype it makes a new class with that name, so you easily have multiple classes with the same name if you are fiddling around in the repl

hiredman04:03:36

The type hint is referring to whatever the most recent class with that name was when the type hinted code was compiled

mars0i04:03:17

Oh, interesting. Thanks @hiredman. I did reload that source file. It sounds like if I recreate all of the deftype objects after I reload the source file containing that function as well as the deftype macro call, the error shouldn't occur.

borkdude08:03:35

I want to read the package name from a .class file without using external dependencies. Does anyone have an example of how this can be done? This should benefit clj-kondo analysis. I don't want to shell out to an external program either (too slow).

borkdude10:03:44

I guess I could just take on a dependency on ASM...

p-himik10:03:16

Seems like you can try and load the class file by its name (without .class and with . instead of /) and then watch for an exception:

$ pwd
/home/p-himik/tmp/java-classpath-test
$ cat Test.java
package aaa;

class Test {
    public static void main(String[] argv) {
        System.out.println("Hello");
    }
}
$ jshell -c .
|  Welcome to JShell -- Version 11.0.14
|  For an introduction type: /help intro

jshell> import java.net.URL;

jshell> URL[] urls = new URL[]{new URL("file:///home/p-himik/tmp/java-classpath-test/")};
urls ==> URL[1] { file:/home/p-himik/tmp/java-classpath-test/ }

jshell> import java.net.URLClassLoader;
  
jshell> URLClassLoader cl = new URLClassLoader(urls);
cl ==> java.net.URLClassLoader@533ddba

jshell> cl.loadClass("Test")
|  Exception java.lang.NoClassDefFoundError: aaa/Test (wrong name: Test)
|        at ClassLoader.defineClass1 (Native Method)
|        at ClassLoader.defineClass (ClassLoader.java:1017)
|        at SecureClassLoader.defineClass (SecureClassLoader.java:174)
|        at BuiltinClassLoader.defineClass (BuiltinClassLoader.java:800)
|        at BuiltinClassLoader.findClassOnClassPathOrNull (BuiltinClassLoader.java:698)
|        at BuiltinClassLoader.loadClassOrNull (BuiltinClassLoader.java:621)
|        at BuiltinClassLoader.loadClass (BuiltinClassLoader.java:579)
|        at ClassLoaders$AppClassLoader.loadClass (ClassLoaders.java:178)
|        at ClassLoader.loadClass (ClassLoader.java:576)
|        at ClassLoader.loadClass (ClassLoader.java:522)
|        at (#5:1)

jshell> 

p-himik10:03:30

As you can see, I never mentioned aaa to jshell - but it does know about it.

p-himik10:03:41

Forgot one command above - of course, I also ran javac Test.java.

p-himik10:03:01

Looking through JDK sources now - maybe there's a reasonable way to get to that aaa/Test without having to parse the exception.

borkdude10:03:42

@U2FRKM4TW clj-kondo should not be using any classloader stuff for this

borkdude10:03:07

it's supposed to be running in a graalvm native-image so you should just treat this as a random binary file without any relation to the classloader of the environment

p-himik10:03:17

Oh... makes sense.

borkdude10:03:36

I think ASM works that way

borkdude10:03:57

so maybe it's not so bad to use that, it seems to be a small library

borkdude15:03:07

@U037U6UBEAW helpful thank you!

saidone15:03:37

you're welcome

saidone15:03:11

excuse me I am a newbie

saidone15:03:51

but here's a crude but working translation:

(defn get-class-name [path]
  (with-open [dis (java.io.DataInputStream. (io/input-stream path))]
    ;; skip first 8 bytes
    (.readLong dis)
    (let [constant-pool-count (dec (.readUnsignedShort dis))
          counter (atom 0)
          classes (atom {})
          strings (atom {})]
      (do
        (while (< @counter constant-pool-count)
          (do
            (case (.read dis)
              1 (swap! strings assoc @counter (.readUTF dis))
              5 (do (.readLong dis) (swap! counter inc))
              6 (do (.readLong dis) (swap! counter inc))
              7 (swap! classes assoc  @counter (.readShort dis))
              8 (.readShort dis)
              (.readInt dis))
            (swap! counter inc)))
        ;; skip access flags
        (.readShort dis)
        (clojure.string/join
         "."
         (butlast
          (clojure.string/split
           (get @strings (- (get @classes (.readUnsignedShort dis)) 2))
           #"/")))))))

borkdude16:03:12

@U037U6UBEAW cool! I'm getting a NPE here:

(prn (get @classes (.readUnsignedShort dis)))

borkdude16:03:32

a nil I mean

saidone16:03:40

can you send me the class you're running it against?

saidone16:03:46

I just tried it only with a half dozen of classes I found on my laptop

borkdude16:03:10

This is clojure.lang.PersistentVector.class

borkdude16:03:28

I'll likely stick with ASM since I don't want to get a lot of problems with this :)

saidone16:03:04

replace (get @strings (- (get @classes (.readUnsignedShort dis)) 2))

saidone16:03:21

with (get @strings (- (get @classes (- (.readUnsignedShort dis) 1)) 1))

borkdude16:03:09

nice, it returns the package name now

borkdude16:03:25

(it also works in babashka I noticed :)

saidone16:03:08

I've not found a more elegant way because of the need to increment the counter inside the loop a couple of times

borkdude16:03:48

I swapped your impl to use volatiles now, should be a little faster

borkdude16:03:35

That comment makes me want to stick with ASM since I won't have to deal with stuff that I don't really know a lot about (parsing class files)

saidone16:03:44

I think ASM is safer

borkdude16:03:12

still cool that you managed to port it. I'm going to keep that example somewhere

saidone16:03:27

a new class format eventually will break that simple parser

saidone16:03:32

I just wanted to practice a little bit šŸ™‚

saidone07:03:07

Double checked against https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html counter need to start from 1 indeed: > A constant_pool index is considered valid if it is greater than zero and less than constant_pool_count and so there's no need to decrement indices for retrieving correct values

saidone07:03:58

also, .skipBytes could be a better semantic choice since we are effectively throwing away those values

saidone07:03:27

The need to skip constant pool entries is also explained here https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4

saidone07:03:58

> In retrospect, making 8-byte constants take two constant pool entries was a poor choice.

saidone07:03:27

so ends up with

saidone07:03:11

5 (do (.skipBytes dis 8) (swap! counter inc))
6 (do (.skipBytes dis 8) (swap! counter inc))

saidone07:03:50

Have a nice day!

borkdude12:03:43

@U037U6UBEAW I converted the while and mutation to a loop and shared it here: https://gist.github.com/borkdude/d02dc3ff1d03d09351e768964983a46b

saidone16:03:54

looks much better now, thanks šŸ™‚

saidone17:03:02

Sorry @U04V15CAJ but I wasn't happy until having correctly parsed at least all 17k classes in my home... First there was an error in the gist at line 19 where the counter must be incremented by 2 (unfortunately, "8-byte constants take up two entries in the constant_pool table of the class file"), and some tags were not considered (most prominently CONSTANT_MethodHandle_info that consumes 3 bytes and caused some quirks). Also made the counter start from 1 ("constant_pool index is considered valid if it is greater than zero and less than constant_pool_count") so now indices on table are correct and there's no more need to decrease them when retrieving values...

saidone17:03:27

(defn class->package
  "Implementation by Marco Marini."
  [class-file]
  (with-open [dis (java.io.DataInputStream. (io/input-stream class-file))]
    ;; skip first 8 bytes
    (.skipBytes dis 8)
    (let [constant-pool-count (.readUnsignedShort dis)
          [strings classes]
          (loop [counter 1
                 classes {}
                 strings {}]
            (if (< counter constant-pool-count)
              (case (.read dis)
                1 (recur (inc counter) classes (assoc strings counter (.readUTF dis)))
                (5 6) (do (.skipBytes dis 8)
                          (recur (+ 2 counter) classes strings))
                7 (recur (inc counter) (assoc classes counter (.readUnsignedShort dis)) strings)
                (8 16 19 20) (do (.skipBytes dis 2)
                                 (recur (inc counter) classes strings))
                15 (do (.skipBytes dis 3) (recur (inc counter) classes strings))
                (do (.skipBytes dis 4)
                    (recur (inc counter) classes strings)))
              [strings classes]))]
      ;; skip access flags
      (.skipBytes dis 2)
      ;; (prn (get @classes (.readUnsignedShort dis)))
      (str/join
       "."
       (butlast
        (str/split
         (get strings (get classes (.readUnsignedShort dis)))
         #"/"))))))

saidone17:03:41

Thanks again and enjoy the evening šŸ™‚

borkdude17:03:48

Awesome, I'll update the gis!

borkdude12:03:30

I'm now trying @hiredmanā€™s javap-mode in emacs... https://github.com/hiredman/javap-mode Doesn't seem to work yet, but it would be a nice solution for when lsp-mode navigates us to a .class file

hiredman15:03:40

Very early effort, likely to have bitrotted, I seem to recall running into another package that did the same thing but better but I can't recall where

hiredman16:03:12

There are snippets like https://gist.github.com/skeeto/3178747 floating around

hiredman16:03:08

I also could have sworn I had a gist with some clojure code for parsing class files, but I couldn't find it

borkdude16:03:46

@UKFSJSM38 and I are now looking for a way to embed CFR (java decompiler) in clojure-lsp so it will automatically do it for every editor

Nundrum15:03:49

Is there some clever way to trace into java method calls? Sadly I can't get visualvm up and running right now because Oracle is having some web problems.

ahungry19:03:36

what's the best way to handle clojure + openjfx? On Arch, they're shipped separately and to do the OpenJFX Hello World in pure java requires manually including --module-path and --add-modules or adjusting -cp to point there - I see I can set -Scp with clj, but that drops all the defaults clojure seems to need from it - I came up with this: clj -Scp '/usr/lib/jvm/java-17-openjdk/lib/javafx.swing.jar:/usr/lib/jvm/java-17-openjdk/lib/javafx.graphics.jar:/usr/lib/jvm/java-17-openjdk/lib/javafx.base.jar:/usr/lib/jvm/java-17-openjdk/lib/javafx.controls.jar:'$(clj -Spath 2>/dev/null) which seems to do the trick but feels like it isn't the best way to do it - also, how is the distribution story with clojure+openjfx for a gui (uberjar)? Would users have to deal wth all this hassle? Compared to a more self-contained clojure+swing jar

hiredman19:03:27

You could use a local deps for the javafx jars

thanks3 1
Carlo20:03:52

A question that was sparlkled from a conversation in #beginners: I'm a bit confused by this repl interaction:

Clojure 1.11.0
user=> (defn foo []
         (ns bar)
         (println *ns*)
         (defn baz [] (println "baz"))
         (ns user)
         (println *ns*))
#'user/foo
user=> (foo)
#object[clojure.lang.Namespace 0x56f71edb bar]
#object[clojure.lang.Namespace 0x6fd1660 user]
nil
user=> (bar/baz)
Syntax error compiling at (REPL:1:1).
No such var: bar/baz
user=> (user/baz)
baz
nil
Why is the symbol baz not defined in the bar namespace?

šŸ˜¦ 1
Carlo00:03:53

After reading @U054W022G explanation of ns related function in his book, I now understand that the proper way of writing what I was trying to write is:

(defn foo []
  (create-ns 'bar)
  (intern 'bar 'baz #(println "baz")))
but I still wonder why exactly the earlier version didn't work

teodorlu07:03:23

Also note that ns is a macro, not a function. Meaning the ns call gets expanded when you define the function, not when you run the function. There's also in-ns, which might be doing what you're expecting: https://clojuredocs.org/clojure.core/in-ns I thought it was a function, but I can't find its source in clojure.core.

genmeblog21:03:20

During fixing https://ask.clojure.org/index.php/11672/fastmath-errors-in-1-11 I've encountered some strange behaviour related to symbol unmap in precompiled namespace. More in a thread.

genmeblog21:03:27

EDIT: see below for a very minimal example (protocol doesn't matter here) Let's create projecta with two files: projecta.protocols

(ns projecta.protocols
  (:refer-clojure :exclude [abs]))

(defprotocol PAbs
  (abs [object])) 
projecta.core
(ns projecta.core
  (:require [projecta.protocols :as p]))

(deftype SomeType [^double v]
  Object
  (toString [_] (str v))
  p/PAbs
  (abs [_] (SomeType. (clojure.core/abs v))))

(ns-unmap *ns* 'abs)
(defn abs [v] (p/abs v))

genmeblog21:03:57

Let's create projectb with lein project.clj

(defproject projectb "0.1.0-SNAPSHOT"
  :prep-tasks [["compile" "projecta.core"] "javac"]
  :dependencies [[org.clojure/clojure "1.11.0"]
                 [projecta "0.1.0-SNAPSHOT"]])

genmeblog21:03:17

projecta.core/abs is not defined šŸ˜•

$ lein repl
Compiling projecta.core
nREPL server started on port 43847 on host 127.0.0.1 - 
REPL-y 0.5.1, nREPL 0.8.3
Clojure 1.11.0
OpenJDK 64-Bit Server VM 18-ea+36-Ubuntu-1
[...]
user=> (require '[projecta.core :as pa])
nil
user=> (def a (pa/->SomeType -3))
#'user/a
user=> (pa/abs a)
Syntax error compiling at (/tmp/form-init18231071095412228753.clj:1:1).
No such var: pa/abs

genmeblog21:03:12

When projecta.core is constructed as following:

(ns projecta.core
  (:refer-clojure :exclude [abs])
  (:require [projecta.protocols :as p]))

(deftype SomeType [^double v]
  Object
  (toString [_] (str v))
  p/PAbs
  (abs [_] (SomeType. (clojure.core/abs v))))

(defn abs [v] (p/abs v))

genmeblog21:03:24

Everything works as intended

genmeblog21:03:58

$ lein repl
Compiling projecta.core
nREPL server started on port 45563 on host 127.0.0.1 - 
REPL-y 0.5.1, nREPL 0.8.3
Clojure 1.11.0
OpenJDK 64-Bit Server VM 18-ea+36-Ubuntu-1
[...]
user=> (require '[projecta.core :as pa])
nil
user=> (def a (pa/->SomeType -3))
#'user/a
user=> (pa/abs a)
#object[projecta.core.SomeType 0x3131b2ea "3.0"]

genmeblog21:03:08

@U064X3EF3 maybe you can help here?

Alex Miller (Clojure team)21:03:37

Not going to look at it this weekend but I can check it out on Monday

genmeblog21:03:35

Thanks! No rush, I just want to understand what is going on here and why this happens. Enjoy the weekend!

hiredman21:03:30

Struggling to parse the examples, it looks like everything working as intended

hiredman21:03:27

The protocol is defined in the namespace projecta.protocols so that is where the abs function that is part of the protocol is

hiredman21:03:50

Ah, I see the example in the middle with the face

hiredman21:03:06

No, that is correct too

genmeblog21:03:19

Maybe protocol can be misleading, I just copied what I have in fastmath and clojure2d.

genmeblog21:03:14

The difference is that in the first approach I call ns-unmap to remove abs from the namespace. In the second I use refer-clojure

genmeblog22:03:38

The minimal example:

(ns projecta.core)
(ns-unmap *ns* 'abs)
(defn abs [v] (clojure.core/abs v))

genmeblog22:03:38

versus

(ns projecta.core
  (:refer-clojure :exclude [abs]))
(defn abs [v] (clojure.core/abs v))

hiredman22:03:17

Try printing out the value of *ns* before the unmap

genmeblog22:03:08

Both work without precompilation.

genmeblog22:03:58

But when the namespace is precompiled latter contains abs, former not

hiredman22:03:30

I just wonder if the value of *ns* is not what you expect when loading an aot compiled namespace, because compilation is not happening in that case

hiredman22:03:46

I kind of expect it should be, but don't recall

genmeblog22:03:58

After adding (println *ns***)

Compiling projecta.core
#object[clojure.lang.Namespace 0x5d10455d projecta.core]

genmeblog22:03:33

user=> (require '[projecta.core :as pa])
#object[clojure.lang.Namespace 0x2db705a7 projecta.core]
nil
user=> (pa/abs -4)
Syntax error compiling at (/tmp/form-init1901708797312745991.clj:1:1).
No such var: pa/abs

genmeblog22:03:28

hmmm... so compiled namespace is different object than namespace after require. Maybe that's why pa/abs is not visible? It belongs to a different namespace.

hiredman22:03:06

Oh, you know, I bet it is a constant pool thing

hiredman22:03:48

When you aot compile a namespace, all the code is turned into a static init method on the class file that is generated for the ns

hiredman22:03:22

Constants (like vars referenced) are lifted out into static fields

hiredman22:03:37

So for vars you only pay for var resolution once

hiredman22:03:01

But what is happening (my guess) is the def isn't looking up the var again, but reusing the one in the constant pool, which the ns-unmap previous removed from the ns

hiredman22:03:53

Try putting the def in an immediately invoked thunk ((fn [] (def ...))) Not sure if that would work around it if I have the issue right, but it might (the fn gets its own constant pool, but I am not sure when those constants would be initialized)

genmeblog22:03:36

((fn [] (defn abs [v] (clojure.core/abs v))))

hiredman22:03:28

Yeah, and if you don't aot compile, top level forms are effectively each compiled in a think like that, so they don't share a constant pool

genmeblog22:03:46

I need to digest it. I mean, I'm not familiar with the aot path (yet). Thanks a lot anyway.

genmeblog22:03:46

ok, I've read it again, I think I got it.

hiredman22:03:49

Definitely an interesting divergence of behavior between aot and not

šŸ‘ 1