Best practices for C code registration

Working on a Claude Code (CC) skill to understand and create SWI-Prolog C code. Had Claude create a human readable document first for me to read to see what it understood and in checking the information with the actual C code noticed

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Link all foreign language predicates.  The arguments to FRG are:

	FRG(name, arity, function, flags).

Flags almost always is TRACE_ME.  Additional common flags:

	P_TRANSPARENT	Predicate is module transparent
	P_NONDET	Predicate can be resatisfied

Deprecated: the modern interface uses PRED_IMPL()
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

swipl/src/pl-ext.c at c933aa07e8248bdf47fc3c373dcf977ed05c13ea · SWI-Prolog/swipl · GitHub

CC made no mention of the deprecation and since I have no expertise registering C code, or such have to ask. Also tried to find documentation on such in the SWI-Prolog Reference Manual or in this forum.

Do not need anything large, just something to help CC understand what is the best practice to register C code for use with SWI-Prolog.

Also wondering if/how PL_register_foreign() has an affect on registering C code.

Instead of C, try C++ (the newer API).
For example, predicate registration is handled automatically.

1 Like

Thanks.

I’ll put that on the TODO list but no promises.


For coding with specific programming languages I will often disable the LLM web search ability and pass the URLs to where the information exist. Often for SWI-Prolog this is the SWI-Prolog reference manual and also specific GitHub repositories. https://github.com/SWI-Prolog/swipl

Allowing the LLM to use web search produces too many off target replies, outdated information, etc. At times the GitHub code is more accurate than the documentation so worth the extra tokens to search that.

In manually searching for the C++ documentation find it under packages

At present trying to get Claude to understand the standard SWI-Prolog code, which may take some time, packages are something I will have to cherry pick as needed.

However, open to seeing Claude skills that augment what Claude knows to be better with such.

This is used internally. User extensions must use PL_register_foreign().

There are plenty of examples in the various packages.

1 Like

Seems that for one of my current needs, using the SWI-Prolog pack for RocksDB might require me to make use of C++ to get RocksDB pack to work on Windows 11. My goal is get the RocksDB pack to work natively on Windows 11 but it also seems that to avoid WSL will be using MSYS2 as building a full native version has not been accomplished successfully.

Fingers crossed.

If RocksDB compiles, the interface is very likely to work as well.

1 Like

Was able to get both repos working on Windows 11 using MSYS2 with very little problems.

Had Claude create a markdown document based on the steps that worked. Did not double check them by redoing all of the work but a visual inspection looked correct.


With some luck, simply running this should do the trick as well, no?

swipl pack install rocksdb

At least, it should be possible to make this work …

I have not tried to use the SWI-Prolog packs as indented yet, want to see the code work first.


I’m currently doing a full build of SWI-Prolog interface for RocksDB and rocks-predicates using vcpkg with MSVC as native Windows 11 build. So far, this appears to be producing working code. It also looks like Claude created a CMakeLists.txt to do the build SWI-Prolog interface for RocksDB .

At the moment, I’m having Claude generate and run a large number of unit tests. The process has been going better than expected: there have been no blockers halting progress, and Claude has been able to work through the bugs and minor hiccups in the generated code with little to no manual intervention.

@jan

In reviewing some of the summary documentation Claude created for testing SWI-Prolog interface for RocksDB it noted:

Test Isolation - Preventing data pollution with unique database paths per test

That looked odd so in manually reading SWI-Prolog interface for RocksDB Readme.md it noted

The wrapper provides most of functionality of RocksDB.
… are not supported, nor are the following features (this is not an exhaustive list):

  • DB paths

wondering if this might be related to the reason Claude switched to using unique database paths per test.

Currently having Claude look for a proper resolution to testing with same and different databases so that it does not have to use unique database paths per test, which seems like a workaround.


Claude came up with this after working on the problem

The Problem

You correctly identified that my initial approach of using unique database paths per test was a workaround, not a proper solution. For persistent databases like RocksDB, programmatic data clearing doesn’t work - the database maintains internal state that can’t be easily reset without deleting the entire database directory.

The Correct Solution

Suite-level isolation with order-independent test assertions:

  1. Suite-level setup/cleanup - Each test suite gets a fresh database that’s deleted and recreated before the suite runs, then deleted after all tests complete
  2. Tests share the database - This is intentional! It allows testing persistence across operations (e.g., write data, close DB, reopen DB, verify data persists)
  3. Order-independent assertions - Instead of checking exact counts (length(Preds, 3)), check for specific data existence (memberchk(pred1/1, Preds))

I don’t know. The current version is for a large part the work of @peter.ludemann

Not really sure what this means. PlUnit defines test units (begin_tests/2 … end_tests/1). Units may be executed concurrently. Individual tests are executed in the order of appearance, though using run_tests/2 one may not execute all tests within a unit.



Asked Claude, this is what it replied.

Note: There is a Summary Table at the end of the reply.


Suite-Level Isolation with Order-Independent Test Assertions

This is the proper testing strategy for persistent databases (RocksDB, library(persistency)) where you can’t easily clear data without deleting the entire database.

Core Concept

Suite-level isolation = Each test suite gets a fresh database, but tests within that suite share the same database instance.

Order-independent assertions = Tests check for specific data existence rather than exact counts, so they pass regardless of what previous tests added.


Example 1: Basic Suite-Level Isolation

% The suite setup runs ONCE before all tests
% The suite cleanup runs ONCE after all tests
:- begin_tests(my_database_tests, [
    setup(setup_test_db(mydb)),      % Creates fresh DB
    cleanup(cleanup_test_db(mydb))   % Deletes DB
]).

test(first_test) :-
    rdb_open('dbs/test_mydb', _),
    rdb_assertz('dbs/test_mydb', item(apple)),
    rdb_close('dbs/test_mydb').

test(second_test) :-
    rdb_open('dbs/test_mydb', _),
    rdb_assertz('dbs/test_mydb', item(banana)),
    % Database now has: item(apple) from first_test
    %                   item(banana) from this test
    rdb_close('dbs/test_mydb').

test(third_test) :-
    rdb_open('dbs/test_mydb', _),
    % Database has: item(apple), item(banana)
    findall(X, rdb_clause('dbs/test_mydb', item(X), true), Items),
    % Items = [apple, banana] or [banana, apple]
    % DON'T check exact count or order!
    rdb_close('dbs/test_mydb').

:- end_tests(my_database_tests).

What happens:

  1. setup_test_db(mydb) runs → Creates fresh dbs/test_mydb/ directory
  2. first_test runs → Adds item(apple)
  3. second_test runs → Sees item(apple) from first test, adds item(banana)
  4. third_test runs → Sees both items
  5. cleanup_test_db(mydb) runs → Deletes entire dbs/test_mydb/ directory

Example 2: Order-Independent Assertions (WRONG vs RIGHT)

:cross_mark: WRONG - Brittle to test order

test(count_users) :-
    rdb_assertz('dbs/test_app', user(alice)),
    rdb_assertz('dbs/test_app', user(bob)),
    findall(U, rdb_clause('dbs/test_app', user(U), true), Users),
    assertion(length(Users, 2)).  % FAILS if previous test added users!

Why it fails: If a previous test added user(charlie), this test expects 2 users but finds 3.

:white_check_mark: CORRECT - Check for specific data

test(has_users) :-
    rdb_assertz('dbs/test_app', user(alice)),
    rdb_assertz('dbs/test_app', user(bob)),
    % Check that OUR specific users exist
    rdb_clause('dbs/test_app', user(alice), true),
    rdb_clause('dbs/test_app', user(bob), true).
    % Don't care if other users exist too!

Why it works: Only checks that alice and bob exist, regardless of other data.

:white_check_mark: ALSO CORRECT - Check membership, not count

test(multiple_users) :-
    rdb_assertz('dbs/test_app', user(alice)),
    rdb_assertz('dbs/test_app', user(bob)),
    findall(U, rdb_clause('dbs/test_app', user(U), true), Users),
    % Check our users are in the list
    memberchk(alice, Users),
    memberchk(bob, Users).
    % Don't check length(Users, 2) - there might be more!

Example 3: Testing Persistence (The POINT of Shared DB)

:- begin_tests(persistence_tests, [
    setup(setup_test_db(persist)),
    cleanup(cleanup_test_db(persist))
]).

% Test 1: Write data and close
test(write_and_close) :-
    rdb_open('dbs/test_persist', _),
    rdb_assertz('dbs/test_persist', config(version, '1.0')),
    rdb_close('dbs/test_persist').

% Test 2: Reopen and verify data SURVIVED the close
test(reopen_and_read) :-
    rdb_open('dbs/test_persist', _),
    % This SHOULD see config(version, '1.0') from previous test!
    rdb_clause('dbs/test_persist', config(version, V), true),
    assertion(V == '1.0'),  % Verifies persistence!
    rdb_close('dbs/test_persist').

% Test 3: Add more data
test(accumulate_data) :-
    rdb_open('dbs/test_persist', _),
    rdb_assertz('dbs/test_persist', config(author, 'Eric')),
    % Database now has TWO config facts
    findall(K-V, rdb_clause('dbs/test_persist', config(K,V), true), Configs),
    memberchk(version-'1.0', Configs),  % From test 1
    memberchk(author-'Eric', Configs),  % From this test
    rdb_close('dbs/test_persist').

:- end_tests(persistence_tests).

The point: These tests intentionally share data to verify that:

  • Data survives rdb_close/rdb_open cycles
  • Multiple facts can coexist
  • The database is truly persistent

Example 4: Multiple Test Suites (Isolation BETWEEN Suites)

% Suite 1: User management tests
:- begin_tests(user_tests, [
    setup(setup_test_db(users)),    % Fresh 'dbs/test_users'
    cleanup(cleanup_test_db(users))
]).

test(add_user) :-
    rdb_assertz('dbs/test_users', user(alice)).

test(list_users) :-
    findall(U, rdb_clause('dbs/test_users', user(U), true), Users),
    memberchk(alice, Users).  % Sees alice from add_user

:- end_tests(user_tests).

% Suite 2: Product management tests
:- begin_tests(product_tests, [
    setup(setup_test_db(products)),    % Different fresh 'dbs/test_products'
    cleanup(cleanup_test_db(products))
]).

test(add_product) :-
    rdb_assertz('dbs/test_products', product(widget)).

test(list_products) :-
    findall(P, rdb_clause('dbs/test_products', product(P), true), Products),
    assertion(Products == [widget]).  
    % Can check exact count! No previous tests in THIS suite added products

:- end_tests(product_tests).

Isolation:

  • user_tests suite uses dbs/test_users/ (isolated from products)
  • product_tests suite uses dbs/test_products/ (isolated from users)
  • Each suite starts with completely fresh database
  • Tests within each suite share their suite’s database

Example 5: Real-World Scenario - RDF Triples

:- begin_tests(rdf_tests, [
    setup(setup_test_db(rdf)),
    cleanup(cleanup_test_db(rdf))
]).

% ❌ WRONG - Expects exact count
test(store_two_triples_WRONG) :-
    rdb_assertz('dbs/test_rdf', rdf(subject1, pred1, obj1)),
    rdb_assertz('dbs/test_rdf', rdf(subject1, pred2, obj2)),
    findall(S-P-O, rdb_clause('dbs/test_rdf', rdf(S,P,O), true), Triples),
    assertion(length(Triples, 2)).  % FAILS if previous test added triples!

% âś… CORRECT - Check specific triples exist
test(store_two_triples_RIGHT) :-
    rdb_assertz('dbs/test_rdf', rdf(subject1, pred1, obj1)),
    rdb_assertz('dbs/test_rdf', rdf(subject1, pred2, obj2)),
    % Just verify OUR triples were stored
    rdb_clause('dbs/test_rdf', rdf(subject1, pred1, obj1), true),
    rdb_clause('dbs/test_rdf', rdf(subject1, pred2, obj2), true).

% âś… CORRECT - Query specific subject
test(query_by_subject) :-
    rdb_assertz('dbs/test_rdf', rdf(person1, knows, person2)),
    rdb_assertz('dbs/test_rdf', rdf(person1, likes, pizza)),
    % Check we can query by subject
    rdb_clause('dbs/test_rdf', rdf(person1, knows, person2), true),
    rdb_clause('dbs/test_rdf', rdf(person1, likes, pizza), true).

:- end_tests(rdf_tests).

Example 6: When You DON’T Want Order-Independent (Use Exact Counts)

:- begin_tests(isolated_test, [
    setup(setup_test_db(iso)),
    cleanup(cleanup_test_db(iso))
]).

% This is the ONLY test in the suite, so we can check exact counts!
test(exactly_three_items) :-
    rdb_assertz('dbs/test_iso', item(a)),
    rdb_assertz('dbs/test_iso', item(b)),
    rdb_assertz('dbs/test_iso', item(c)),
    findall(X, rdb_clause('dbs/test_iso', item(X), true), Items),
    assertion(length(Items, 3)),  % Safe! No other tests in suite
    assertion(Items == [a, b, c]).

:- end_tests(isolated_test).

When exact counts are okay:

  • Only one test in the suite
  • First test in the suite (no previous tests added data)
  • You’re testing a complete workflow in a single test

Summary Table

Approach Isolation Level When to Use Example
Suite-level isolation Clean DB per suite Persistent databases (RocksDB, persistency) setup(setup_test_db(name)) on begin_tests
Order-independent assertions Tests share DB When tests accumulate data memberchk(pred/1, List) instead of length(List, N)
Test-level isolation Clean DB per test Non-persistent databases (dynamic facts) setup(retractall(fact(_))) on each test
Subprocess isolation Complete process isolation Rare cases needing total isolation process_create(swipl, ...)

The key insight: For persistent databases, suite-level isolation IS the correct pattern. Trying to clear data per test defeats the purpose of testing persistence!