Having all sorts of problems with libeditline

I’m using 9.3.24 with a hashbang script, but for the life of me I can’t get libeditline to work properly. It appears that user_input is already wrapped before my code is executed. If I call el_wrap/4 in my code the app spins off to infinity as soon as I enter any input. If I don’t call el_wrap, none of my edit config calls such as el_history seem to have any effect, history isn’t saved, arrow keys don’t work etc.

Is it necessary to call el_unwrap, first, even though the docs have warnings about filehandle leaks?

How on earth is this supposed to work?

Could you provide us with the basic setup you are working with, i.e., the hashbang script and how you use libedit? That makes it a lot easier to comment.

I’m trying to get a minimal reproducible case together, I’ll send that out when I’ve replicated the issue. But at the moment I can’t even get current_prolog_flag(readline, F) to work, it blows up beacuse apparently the readline flag doesn’t exist…

#!/opt/swipl/bin/swipl

:- initialization(main, main).

main(_) :-
  current_prolog_flag(readline, F),
  writeln(F)
.

Here’s a cut down version. With the call to el_wrap('test', user_input, user_output, user_error) commented out it works fine. With it uncommented the first 2 lines are read and echoed OK, but the third line is echoed but not read in, and when return is pressed the process spins off to oblivion, consuming 100% CPU. Platform is Debian 12.11.

#!/opt/swipl/bin/swipl

:- initialization(main, main).
  
main(_) :-
  prompt(P, '>> '),

  prompt1('Enter Line1: '),
  read_line_to_string(user_input, Line1),
  format('Line 1 is "~w"~n', [Line1]),
    
  prompt1('Enter Line2: '),
  read_line_to_string(user_input, Line2),
  format('Line 2 is "~w"~n', [Line2]),
  
  writeln('Initialising libeditline'),
  el_wrap('test', user_input, user_output, user_error),
  el_history(user_input, setsize(100)),
  el_history(user_input, setunique(true)),
  (exists_file('history') -> el_read_history(user_input, 'history') ; true),
    
  prompt1('Enter Line3: '),
  read_line_to_string(user_input, Line3),
  format('Line 3 is "~w"~n', [Line3]),
  el_add_history(user_input, Line3),

  prompt1('Enter Line4: '),
  read_line_to_string(user_input, Line4),
  format('Line 4 is "~w"~n', [Line4]),
  el_add_history(user_input, Line4), 
  
  prompt(_, P),
  el_write_history(user_input, 'history')
.   

An aside which doesn’t fix the problem I’m having but… In libedit4pl.c there’s this comment:

   /*  FIXME: We should close the FILE*, but fclose() also closes the
    *  underlying descriptor.
    */

Couldn’t you just dup() the fds before you you fopen() them? Then fclose() would just close the dup()'d fd and not the original one? Not tested and I’m hacking about in ignorance, but something like this:

diff --git i/libedit4pl.c w/libedit4pl.c
index 4004996..f1cc60c 100644
--- i/libedit4pl.c
+++ w/libedit4pl.c
@@ -839,9 +839,10 @@ pl_wrap(term_t progid, term_t tin, term_t tout, term_t terr)
     { el_context *ctx = alloc_context(fd_in);
       FILE *fin, *fout, *ferr;
 
-      fin  = fdopen(fd_in, "r");
-      fout = fdopen(fd_out, "w");
-      ferr = fdopen(fd_err, "w");
+      /* dup() stdin/out/err so we can safely fclose() them */
+      fin  = fdopen(dup(fd_in), "r");
+      fout = fdopen(dup(fd_out), "w");
+      ferr = fdopen(dup(fd_err), "w");
 
       setlinebuf(fin);
       setlinebuf(fout);
@@ -979,9 +980,14 @@ pl_unwrap(term_t tin)
     history_end(ctx->history);
     el_end(ctx->el);
 
-    /*  FIXME: We should close the FILE*, but fclose() also closes the
-     *  underlying descriptor.
-     */
+    /* We dup()'d stdin/out/err so we can fclose() them here. */
+    FILE *fd;
+    el_get(ctx->el, EL_GETFP, 0, &fd);
+    fclose(fd);
+    el_get(ctx->el, EL_GETFP, 1, &fd);
+    fclose(fd);
+    el_get(ctx->el, EL_GETFP, 2, &fd);
+    fclose(fd);
 
     PL_free(ctx);

I assume that would address the memory leak comment in the docs?

The stream is already wrapped. The C debugger shows this as a loop in writing. Probably the library should raise an error if the stream is already wrapped … It works if I add this at the start of main/1:

  (   el_wrapped(user_input)
  ->  writeln("Wrapped"),
      el_unwrap(user_input)
  ;   true
  ),

Thanks for the tip. It is surely worth checking out. I won’t bother too much about the memory leak though. How often would an application use libedit temporary?

There are a couple of problems even if you do that though. The following script:

main(_) :-
  (el_wrapped(user_input) -> writeln("wrapped") ; writeln("not wrapped")),
  (current_prolog_flag(readline, F) -> writeln(F) ; writeln("flag does not exist"))

displays:

wrapped
flag does not exist

Which is inconsistent - stdin is being automatically wrapped at startup, but the prolog flag says it hasn’t been. At some point the flag does appear, I haven’t figured out exactly what triggers that.

I don’t think stdin should be wrapped by default, that should only be done if the interactive top level is actually entered AND if libeditline hasn’t already been enabled.

If you call el_unwrap and then rewrap with your own config, libeditline doesn’t work, or at least not for me. Calls to el_add_history seem to have no effect and the saved history is empty. So it looks like once libeditline is enabled on a stream it can’t be disabled and re-enabled successfully. There are several problems with that - it means if I don’t do any libeditline config at all for my REPL I inherit the prolog top-level libedit config. That means history scrolling show the prolog history intermixed with that of my REPL, and TAB-completion shows a list of prolog predicates, which is most certainly not what I want. Because of the auto-install of libedit on stdin the only option is for me to just disable it altogether - although it’s unclear if I should do that with el_unwrap or by setting the prolog readline flag to false.

In my case, frequently. I want to be able to drop into the prolog top-level on request, then back into my custom REPL. That doesn’t work correctly.

That is also what happens. But, calling el_wrapped/1 causes library(editline) to be loaded and loading the library initializes it on the current I/O streams. That is a dubious. Also the toplevel manages this flag rather than the library itself.

Your example works fine for me and the history is saved. This is using MacOS (I’m traveling). There is no mixup of the normal toplevel either. What is your setup and versions?

If we can avoid the leak using dup(), nice. Else you’ll need a very long session for this to be an issue …

So far the main issue seems to be that the library initializes on load. The other issue is why it doesn’t work for you. Is there anything in your init.pl that may break this? Try swipl -f none

1 Like

I’ve rechecked with the demo script I posted earlier, with the el_wrap call removed. That doesn’t spin, but history scroll / saving does not work.

Config: Debian 12.11, libeditline 3.1.20021030-2. SWIPL pulled from github and built with the V9.3.24 tag checked out.

My init.pl is empty and -f none makes no difference.

What version of libeditline are you using?

As an aside, thank you very much for all the help you’ve given, it’s much appreciated! :smiling_face: