Using call_cleanup/2. Trying to understand when to use it or others predicates of the same intention

For this test case gzip_ascii in package zlib

Here an open and close are used without call_cleanup/2

gzopen('plunit-tmp.gz', write, ZOut, [type(text), encoding(utf8)]),
set_stream(ZOut, newline(posix)),
format(ZOut, '~s', [ReferenceCodes]),
close(ZOut)

The documentation states

Do not use call_cleanup/2 if you perform side-effects prior to calling that will be undone by Cleanup. Instead, use setup_call_cleanup/3 with an appropriate first argument to perform those side-effects.

Is it correct that format(ZOut, '~s', [ReferenceCodes]) is a side effect and the reason call_cleanup/2 is not used?

Here an open and close are used with call_cleanup/2 as expected.

gzopen('plunit-tmp.gz', read, ZIn, [type(text), encoding(utf8)]),
set_stream(ZIn, newline(posix)),
call_cleanup(read_stream_to_codes(ZIn, Codes), close(ZIn))

In general should call_cleanup/2 or others predicates of the same intention be used with any open and close of a stream/file?

Currently I equate this concept to C# using statement with resources. Is that a valid way to begin to understand this concept?

1 Like

The correct way is

    setup_call_cleanup(
        gzopen('plunit-tmp.gz', read, ZIn, [type(text), encoding(utf8)]),
        (  set_stream(ZIn, newline(posix)),
           read_stream_to_codes(ZIn, Codes)
        ),
        close(ZIn)).

Thus, the Setup handler creates a resource, the Call uses it and the Cleanup destroys it. The contract it that if Setup succeeds, Cleanup will be called eventually (provided Call terminates).

This test code is old stuff.

P.s You seem to have adopted the set_stream(ZIn, newline(posix)). This was a particular hack for the test cases that test all ASCII characters in text mode, but the test data uses CR. Normally the text streams do CR/LF translation on I/O and that should be what you want. Otherwise you typically use a binary stream there is no translation.

2 Likes

That is correct.

Parsing PDF files is not as simple as I expected. As we know most non-PDF files are all text, or all binary, or all compressed binary. From what I have seen so far a single PDF file can have text, binary and compressed binary in one file. Then the compressed binary uncompresses into text or binary. Quite a learning exercise.

SWI-Prolog allows for set_stream(S, encoding(octet)) to switch to binary mode at any time. Later, you can use e.g., set_stream(S, encoding(utf8)). Also interesting is the stream_range_open/3 from the HTTP package that allows you to create a stream of a certain length from a longer stream. This is used for HTTP keep-alive connections where we create a range stream with the length given by the Content-length from the raw socket stream.

2 Likes

encoding(octet) is nice and getting me out of some problems, one specifically is that PDF text object streams use the unprintable characters SOH 0x01 and STX 0x02. e.g.

Capture

Using encoding(ascii) results in

Warning: ‘2_0_pdf_text.txt’:599:0: non-ASCII character

while using encoding(octet) nicely allows for the use normally as text and doesn’t give the warning.

Use case related to asserta/2

From test_read.pl

	setup_call_cleanup(
	    asserta((user:thread_message_hook(Term, Kind, _) :-
		        \+ \+ (prolog_load_context(variable_names, VarNames),
			       bind_variable_names(VarNames),
			       assertz(message(Term)))), Ref),
	    once(Goal),
	    erase(Ref))

Use case related to set_prolog_flag/2

From thread_agc.pl

	current_prolog_flag(agc_margin, Old),
	set_prolog_flag(agc_margin, 1000),
	call_cleanup(test(Threads, Count),
		     set_prolog_flag(agc_margin, Old)).

Use case related to process_create/3

From git.pl

setup_call_cleanup(
    process_create(path(git), Argv1,
                   [ stdout(pipe(Out)),
                     stderr(pipe(Error)),
                     process(PID)
                   | Extra
                   ]),
    call_cleanup(
        ( read_stream_to_codes(Out, OutCodes, []),
          read_stream_to_codes(Error, ErrorCodes, [])
        ),
        process_wait(PID, Status)),
    close_streams([Out,Error]))

Use case related to setting value based on exception using catch/3

From statistics.pl

call_cleanup(catch(Goal, E, (report(State0,10), throw(E))),Det = true),
    time_true(State0),
    (   Det == true
    ->  !
    ;   true
    )

test.pl has a predicate with multiple clauses using call_cleanup/2. Some of the use cases are noted above, some are used for testing throw/1 and one I don’t understand

% check handling of CHP_TOP
% notrace/1 does a call-back via C
notrace(call_cleanup(true, true)).

Looks like a test that was added after a bug report. It will only make sense if you dive deep into the implementation of the call_cleanup family. Might also be irrelevant as the implementation was changed a couple of times. It is a practical, but pretty nasty primitive to implement :slight_smile:

@jan Thanks for responding. Wasn’t expecting a response, but nice to see the feedback. :slightly_smiling_face:

I really enjoyed researching the use cases for call_cleanup predicates and it opened a door to many ideas for me on how to use SWI-Prolog in the real world when working with system and ethos related problems.

Hopefully others will find this just as useful, maybe even @jamesnvc might want to expand on this into a post at Prolog Hub

1 Like