Fix #571: How to redirect "cout" and "print" to a microsoft Windows 10 window? (#657)

* Fix #571: Add per-instance IO redirection via set_print_handler/set_println_handler

The print_string and println_string functions were previously registered as static
functions writing directly to stdout, making it impossible to redirect ChaiScript
output to custom destinations (e.g., GUI windows, loggers, or buffers). This moves
their registration from Bootstrap::bootstrap() to ChaiScript_Basic::build_eval_system()
as lambdas that dispatch through configurable std::function handlers, allowing each
ChaiScript instance to independently redirect its output via set_print_handler() and
set_println_handler().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: add IO redirection section to cheatsheet

Documents set_print_handler() and set_println_handler() with usage
examples for GUI embedding and output capture.

Requested by @lefticus in PR #657 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: define println in terms of print, expose set_print_handler to ChaiScript

Remove separate println_handler — println_string now dispatches through the
single print handler with a newline appended. Only set_print_handler is
needed to redirect all output. The set_print_handler function is also
registered in the ChaiScript engine, so scripts can capture and redirect
their own output.

Requested by @lefticus in PR #657 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: populate null print handler when No_IO is set

When No_IO is active, the default m_print_handler is now a no-op instead
of writing to stdout. The stdout handler is only installed when No_IO is
not set. Users can still override the handler via set_print_handler()
even with No_IO enabled.

Requested by @lefticus in PR #657 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-11 18:58:09 -06:00 committed by GitHub
parent dcdab50efd
commit bcd07e05cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 169 additions and 15 deletions

View File

@ -876,5 +876,42 @@ m.is_type("MyClass") // true (checks the ChaiScript class name)
* `from_json` converts a JSON string into its strongly typed (map, vector, int, double, string) representations * `from_json` converts a JSON string into its strongly typed (map, vector, int, double, string) representations
* `to_json` converts a ChaiScript object (either a `Object` or one of map, vector, int, double, string) tree into its JSON string representation * `to_json` converts a ChaiScript object (either a `Object` or one of map, vector, int, double, string) tree into its JSON string representation
## IO Redirection
By default, ChaiScript's `print()` and `puts()` functions write to stdout. You can redirect
output on a per-instance basis by setting a single print handler. Both `println_string`
(used by `print()`) and `print_string` (used by `puts()`) dispatch through the same handler —
`println_string` simply appends a newline before calling it.
```cpp
chaiscript::ChaiScript chai;
// Redirect all output (print_string and println_string both use this handler)
chai.set_print_handler([](const std::string &s) {
my_log_window.append(s);
});
```
This is useful for embedding ChaiScript in GUI applications, logging frameworks, or any
context where stdout is not the desired output destination.
```cpp
// Example: capture all output to a string
std::string captured;
chai.set_print_handler([&captured](const std::string &s) {
captured += s;
});
chai.eval("print(42)"); // captured == "42\n"
chai.eval("puts(\"hi\")"); // captured == "42\nhi"
```
The print handler can also be set from within ChaiScript itself via `set_print_handler`:
```chaiscript
// Redirect output from within a script
set_print_handler(fun(s) { my_custom_log(s) })
```
## Extras ## Extras
ChaiScript itself does not provide a link to the math functions defined in `<cmath>`. You can either add them yourself, or use the [ChaiScript_Extras](https://github.com/ChaiScript/ChaiScript_Extras) helper library. (Which also provides some additional string functions.) ChaiScript itself does not provide a link to the math functions defined in `<cmath>`. You can either add them yourself, or use the [ChaiScript_Extras](https://github.com/ChaiScript/ChaiScript_Extras) helper library. (Which also provides some additional string functions.)

View File

@ -830,7 +830,8 @@ namespace chaiscript {
std::make_unique<parser::ChaiScript_Parser<eval::Noop_Tracer, optimizer::Optimizer_Default>>(), std::make_unique<parser::ChaiScript_Parser<eval::Noop_Tracer, optimizer::Optimizer_Default>>(),
std::move(t_modulepaths), std::move(t_modulepaths),
std::move(t_usepaths), std::move(t_usepaths),
std::move(t_opts)) { std::move(t_opts),
std::find(t_lib_opts.begin(), t_lib_opts.end(), Library_Options::No_IO) != t_lib_opts.end()) {
} }
}; };
} // namespace chaiscript } // namespace chaiscript

View File

@ -437,10 +437,10 @@ namespace chaiscript::bootstrap {
m.add(fun(&Build_Info::compiler_id), "compiler_id"); m.add(fun(&Build_Info::compiler_id), "compiler_id");
m.add(fun(&Build_Info::debug_build), "debug_build"); m.add(fun(&Build_Info::debug_build), "debug_build");
if (!t_no_io) { // print_string and println_string are registered in ChaiScript_Basic::build_eval_system()
m.add(fun(&print), "print_string"); // to support per-instance IO redirection via set_print_handler.
m.add(fun(&println), "println_string"); // When No_IO is set, the functions are still registered but the default handler
} // is a no-op, so users can provide their own print handlers without any stdout output.
m.add(dispatch::make_dynamic_proxy_function(&bind_function), "bind"); m.add(dispatch::make_dynamic_proxy_function(&bind_function), "bind");

View File

@ -79,6 +79,8 @@ namespace chaiscript {
std::map<std::string, std::function<Namespace &()>> m_namespace_generators; std::map<std::string, std::function<Namespace &()>> m_namespace_generators;
std::function<void(const std::string &)> m_print_handler = [](const std::string &) noexcept {};
/// Evaluates the given string in by parsing it and running the results through the evaluator /// Evaluates the given string in by parsing it and running the results through the evaluator
Boxed_Value do_eval(const std::string &t_input, const std::string &t_filename = "__EVAL__", bool /* t_internal*/ = false) { Boxed_Value do_eval(const std::string &t_input, const std::string &t_filename = "__EVAL__", bool /* t_internal*/ = false) {
try { try {
@ -119,11 +121,24 @@ namespace chaiscript {
chaiscript::detail::Dispatch_Engine &get_eval_engine() noexcept { return m_engine; } chaiscript::detail::Dispatch_Engine &get_eval_engine() noexcept { return m_engine; }
/// Builds all the requirements for ChaiScript, including its evaluator and a run of its prelude. /// Builds all the requirements for ChaiScript, including its evaluator and a run of its prelude.
void build_eval_system(const ModulePtr &t_lib, const std::vector<Options> &t_opts) { void build_eval_system(const ModulePtr &t_lib, const std::vector<Options> &t_opts, const bool t_no_io = false) {
if (t_lib) { if (t_lib) {
add(t_lib); add(t_lib);
} }
if (!t_no_io) {
m_print_handler = [](const std::string &s) noexcept {
fwrite(s.c_str(), 1, s.size(), stdout);
};
}
m_engine.add(fun([this](const std::string &s) { m_print_handler(s); }), "print_string");
m_engine.add(fun([this](const std::string &s) { m_print_handler(s + "\n"); }), "println_string");
m_engine.add(fun([this](const std::function<void(const std::string &)> &t_handler) {
m_print_handler = t_handler;
}), "set_print_handler");
m_engine.add(fun([this]() { m_engine.dump_system(); }), "dump_system"); m_engine.add(fun([this]() { m_engine.dump_system(); }), "dump_system");
m_engine.add(fun([this](const Boxed_Value &t_bv) { m_engine.dump_object(t_bv); }), "dump_object"); m_engine.add(fun([this](const Boxed_Value &t_bv) { m_engine.dump_object(t_bv); }), "dump_object");
m_engine.add(fun([this](const Boxed_Value &t_bv, const std::string &t_type) { return m_engine.is_type(t_bv, t_type); }), "is_type"); m_engine.add(fun([this](const Boxed_Value &t_bv, const std::string &t_type) { return m_engine.is_type(t_bv, t_type); }), "is_type");
@ -257,6 +272,12 @@ namespace chaiscript {
public: public:
/// \brief Set a custom handler for print output, used by both print_string and println_string
/// \param[in] t_handler Function to call with the string to print
void set_print_handler(std::function<void(const std::string &)> t_handler) {
m_print_handler = std::move(t_handler);
}
/// \brief Virtual destructor for ChaiScript /// \brief Virtual destructor for ChaiScript
virtual ~ChaiScript_Basic() = default; virtual ~ChaiScript_Basic() = default;
@ -268,7 +289,8 @@ namespace chaiscript {
std::unique_ptr<parser::ChaiScript_Parser_Base> &&parser, std::unique_ptr<parser::ChaiScript_Parser_Base> &&parser,
std::vector<std::string> t_module_paths = {}, std::vector<std::string> t_module_paths = {},
std::vector<std::string> t_use_paths = {}, std::vector<std::string> t_use_paths = {},
const std::vector<chaiscript::Options> &t_opts = chaiscript::default_options()) const std::vector<chaiscript::Options> &t_opts = chaiscript::default_options(),
const bool t_no_io = false)
: m_module_paths(ensure_minimum_path_vec(std::move(t_module_paths))) : m_module_paths(ensure_minimum_path_vec(std::move(t_module_paths)))
, m_use_paths(ensure_minimum_path_vec(std::move(t_use_paths))) , m_use_paths(ensure_minimum_path_vec(std::move(t_use_paths)))
, m_parser(std::move(parser)) , m_parser(std::move(parser))
@ -303,7 +325,7 @@ namespace chaiscript {
m_module_paths.insert(m_module_paths.begin(), dllpath + "/"); m_module_paths.insert(m_module_paths.begin(), dllpath + "/");
} }
#endif #endif
build_eval_system(t_lib, t_opts); build_eval_system(t_lib, t_opts, t_no_io);
} }
#ifndef CHAISCRIPT_NO_DYNLOAD #ifndef CHAISCRIPT_NO_DYNLOAD

View File

@ -1360,17 +1360,26 @@ TEST_CASE("ChaiScript_Basic No_Stdlib option disables all standard library funct
CHECK_THROWS(chai.eval("from_json(\"[1,2,3]\")")); CHECK_THROWS(chai.eval("from_json(\"[1,2,3]\")"));
} }
TEST_CASE("ChaiScript_Basic No_IO option disables print functions") { TEST_CASE("ChaiScript_Basic No_IO option uses null handler by default") {
chaiscript::ChaiScript_Basic chai(chaiscript::Std_Lib::library({chaiscript::Library_Options::No_IO}), chaiscript::ChaiScript_Basic chai(chaiscript::Std_Lib::library({chaiscript::Library_Options::No_IO}),
create_chaiscript_parser(), create_chaiscript_parser(),
{}, {},
{}, {},
{chaiscript::Options::No_Load_Modules, chaiscript::Options::No_External_Scripts}); {chaiscript::Options::No_Load_Modules, chaiscript::Options::No_External_Scripts},
true);
CHECK_THROWS(chai.eval("print_string(\"hello\")")); // print_string and println_string should still be available via the handler mechanism
CHECK_THROWS(chai.eval("println_string(\"hello\")")); // but the default handler is a no-op (no stdout output)
CHECK_NOTHROW(chai.eval("print_string(\"hello\")"));
CHECK_NOTHROW(chai.eval("println_string(\"hello\")"));
CHECK(chai.eval<int>("5 + 3") == 8); CHECK(chai.eval<int>("5 + 3") == 8);
CHECK_NOTHROW(chai.eval("var v = Vector()")); CHECK_NOTHROW(chai.eval("var v = Vector()"));
// Users can set their own print handler even with No_IO
std::string captured;
chai.set_print_handler([&captured](const std::string &s) { captured += s; });
chai.eval("print_string(\"redirected\")");
CHECK(captured == "redirected");
} }
TEST_CASE("ChaiScript_Basic No_Prelude option disables prelude functions") { TEST_CASE("ChaiScript_Basic No_Prelude option disables prelude functions") {
@ -1431,10 +1440,18 @@ TEST_CASE("ChaiScript No_IO option via library options parameter") {
{chaiscript::Options::No_Load_Modules, chaiscript::Options::No_External_Scripts}, {chaiscript::Options::No_Load_Modules, chaiscript::Options::No_External_Scripts},
{chaiscript::Library_Options::No_IO}); {chaiscript::Library_Options::No_IO});
CHECK_THROWS(chai.eval("print_string(\"hello\")")); // print_string and println_string remain available via the handler mechanism
CHECK_THROWS(chai.eval("println_string(\"hello\")")); // but the default handler is a no-op (no stdout output)
CHECK_NOTHROW(chai.eval("print_string(\"hello\")"));
CHECK_NOTHROW(chai.eval("println_string(\"hello\")"));
CHECK(chai.eval<int>("5 + 3") == 8); CHECK(chai.eval<int>("5 + 3") == 8);
CHECK_NOTHROW(chai.eval("var v = Vector()")); CHECK_NOTHROW(chai.eval("var v = Vector()"));
// Users can override the null handler with their own
std::string captured;
chai.set_print_handler([&captured](const std::string &s) { captured += s; });
chai.eval("print_string(\"redirected\")");
CHECK(captured == "redirected");
} }
TEST_CASE("ChaiScript No_Prelude option via library options parameter") { TEST_CASE("ChaiScript No_Prelude option via library options parameter") {
@ -1607,6 +1624,83 @@ TEST_CASE("Issue 625: function_less_than strict-weak ordering with different ari
CHECK(chai.eval<int>("overloaded(3, 2.0)") == 5); CHECK(chai.eval<int>("overloaded(3, 2.0)") == 5);
} }
TEST_CASE("IO redirection with set_print_handler") {
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
std::string captured_output;
// Set custom print handler — both print_string and println_string dispatch through it
chai.set_print_handler([&captured_output](const std::string &s) {
captured_output += s;
});
// Test that puts() uses the custom handler
captured_output.clear();
chai.eval("puts(\"hello\")");
CHECK(captured_output == "hello");
// Test that print() uses the custom handler (println_string appends newline before calling handler)
captured_output.clear();
chai.eval("print(\"world\")");
CHECK(captured_output == "world\n");
// Test that print_string() directly uses the custom handler
captured_output.clear();
chai.eval("print_string(\"direct\")");
CHECK(captured_output == "direct");
// Test that println_string() directly uses the custom handler with newline
captured_output.clear();
chai.eval("println_string(\"direct_ln\")");
CHECK(captured_output == "direct_ln\n");
}
TEST_CASE("IO redirection captures numeric output") {
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
std::string captured_output;
chai.set_print_handler([&captured_output](const std::string &s) {
captured_output += s;
});
chai.eval("print(42)");
CHECK(captured_output == "42\n");
}
TEST_CASE("IO redirection different instances are independent") {
chaiscript::ChaiScript_Basic chai1(create_chaiscript_stdlib(), create_chaiscript_parser());
chaiscript::ChaiScript_Basic chai2(create_chaiscript_stdlib(), create_chaiscript_parser());
std::string output1;
std::string output2;
chai1.set_print_handler([&output1](const std::string &s) { output1 += s; });
chai2.set_print_handler([&output2](const std::string &s) { output2 += s; });
chai1.eval("print(\"from1\")");
chai2.eval("print(\"from2\")");
CHECK(output1 == "from1\n");
CHECK(output2 == "from2\n");
}
TEST_CASE("set_print_handler accessible from ChaiScript") {
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
auto captured = std::make_shared<std::string>();
chai.add(chaiscript::fun([captured](const std::string &s) { *captured += s; }), "test_output_sink");
// Set the print handler from within ChaiScript
chai.eval("set_print_handler(fun(s) { test_output_sink(s) })");
chai.eval("print(\"from_script\")");
CHECK(*captured == "from_script\n");
captured->clear();
chai.eval("puts(\"no_newline\")");
CHECK(*captured == "no_newline");
}
// Regression test: push_back() on script-created vector has no effect when // Regression test: push_back() on script-created vector has no effect when
// vector_conversion is in effect. The bug occurs because dispatch selects // vector_conversion is in effect. The bug occurs because dispatch selects
// the C++ push_back for the converted type over the built-in one, operating // the C++ push_back for the converted type over the built-in one, operating