My general comment on this is to try and revert the embedding, i.e., make the foreign parts that your application requires available as Prolog predicates and write Prolog That of course is not always a great idea. While Prolog looks a bit odd from other languages though, imperative languages look fine from Prolog. Same holds for data abstractions such as tables, trees, graphs, etc.
Still, you may want to embed Prolog The core API is PL_open_query(), PL_next_solution() … PL_close_query(). This triple defines an iterator over a table of solutions. Many high level languages have good abstractions for this. Then there are the (dynamic) data types that include logical variables. My general rule of thumb is to make sure that the Prolog predicates used by the embedding simply use basic types (strings/atoms/numbers), typically making sure all rows of the table use the same types, e.g., the 2nd column is always an integer.
Given that, the embedding is not that hard. You’ll want some abstractions such as for semidet predicates (only one answer, see PL_call_predicate()). And you may want the type conversion abstractions from the C++ interface which also maps exceptions between the two languages.
Bottom line: keep the interface simple from the perspective of the embedding language. Do not try to setup complex goals but use simple goals with atomic arguments when possible and do the necessary fiddling with arguments in Prolog. This, instead of trying to call e.g.
write_term(Stream, Term, [portray(true),numbervars(true), ...]). write a Prolog predicate that passes the options the way you like and call that with the data needed.
Note that if you have data in the foreign code you can translate that to Prolog and pass it to the predicate. You can however also add predicates that allows Prolog to access this data.
We also have to choose between embedding as in linking to a single process and using some interface such as MQI, Pengines, etc. Both have advantages and disadvantages. In general I’d first consider using two processes for high level languages, in particular those that have garbage collection and when there is a mismatch in the threading capabilities (also depending on the need for those in the application). Low-level languages such as C/C++/Rust/… can typically more easily embed SWI-Prolog.
Other good options is to use SWI-Prolog’s HTTP framework and exchange JSON messages or use SWISH. It all depends on the application though. Latency and data exchange required, is there a need for a lot of code development in Prolog or do you interface to a mostly given bit of Prolog code? Etc. Note that also for embedded Prolog you can get toplevel access using ssh and graphical debugging working. The details depend on OS and embedding language.