Hacker News new | past | comments | ask | show | jobs | submit login
ChaiScript – Easy to Use Scripting for C++ (chaiscript.com)
160 points by rsp1984 on July 25, 2019 | hide | past | favorite | 89 comments



I have been looking through it, and examples, and code, and I guess the main question condensing in my mind is one big "Why"? Why use this, why yet another scripting language?

Why not just use Lua, one of JavaScript engines, Python, etc? They all have C++ bindings, all relatively easy to use. All are already used in many C++ apps for this exact purpose... (I guess it's not commonly known in all coder circles, but all those languages can just as easily be embedded in a C++ codebase as this.)


From their GitHub page: "ChaiScript is one of the only embedded scripting language designed from the ground up to directly target C++ and take advantage of modern C++ development techniques, working with the developer how they would expect it to work. Being a native C++ application, it has some advantages over existing embedded scripting languages:

It uses a header-only approach, which makes it easy to integrate with existing projects.

It maintains type safety between your C++ application and the user scripts.

It supports a variety of C++ techniques including callbacks, overloaded functions, class methods, and stl containers."


> ...modern C++ development techniques...

Shipping header only is really admitting that modern C++ still has a lack of technique, sidestepping the lack of modules and packaging for instance.


The decision to go with a headers-only design is only required if you want/need to provide templates.

Furthermore, it's ignorant to conflate headers-only libraries with a problem or a shortcoming.


Being realistic and admitting that C++ has tooling and packaging issues isn't being ignorant. Quite the opposite.

The fact that Header Only is one of the five bolded features of this project indicates that "modern C++ development" includes basically giving up on many desirable features of software projects. It's probably fair enough, but I wouldn't necessarily hold it up over other less "modern" designs like providing a C ABI, which you can still do with C++ using C++17, following basically all of CppCoreGuidelines in the implementation, using state of the art tooling, etc.


> giving up on many desirable features of software projects

Some C++ libraries are header only because they were designed for highest performance possible.

Classic example is std::sort versus qsort, C++ usually wins because inlining.

My favorite example is Eigen, they use template metaprogramming to save RAM traffic. When you write x=a+b+c for matrices or vectors, the library doesn't compute a+b intermediate vector. That C++ expression returns a placeholder object of a weird type, CPU computes a+b+c in a single loop over them, reading a,b,c, and writing to x.


Features are header only for performance. Entire libraries are rarely header only for that reason. Typically it's more for ease of consumption, but there are other major drawbacks to that approach, like having the widest possible API and ABI, making long term maintenance harder.

For instance, on some platforms system headers use preprocessor variables called "major" and "minor". If your code is in source files, you have strong control over whether you care. If your code is all in headers, your don't since all your code is affected by unrelated textual inclusions.


> having the widest possible API and ABI, making long term maintenance harder.

For cases where it matters, C++ can do that just fine.

Look at Direct3D, DirectWrite, Media Foundation. They're written in OO C++, and they use IUnknown ABI from COM. They're not COM objects, the ABI is the only feature they took from there.

Apparently, C++ library developers don't care enough.


That's what I was saying. I was also saying you basically give up on a stable narrow interface when shipping header only.


> you basically give up on a stable narrow interface when shipping header only.

I agree. But in some cases, you also gain performance not achievable with stable interfaces.

It’s about tradeoffs.

I think modern C++ needs stable ABI and well-isolated modules less than it did 10-20 years ago. 20 years ago people wrote everything in C++. Now C++ lost the majority of the market to higher level and safer languages, it’s only used for components where performance really matters. For such components, runtime overhead of stable ABIs sometimes matters.


> sidestepping the lack of modules and packaging for instance.

I'm tired of that nitpicking. These tools happen to be outside of the language for historical reasons and probably C compatibility. It's not in the language because apparently not enough people care.

And I think that the ideal way is standardizing of the OS package management first, not having a package manager (or several) for each language. But it won't happen for OS business reasons.


Considering modules are a major feature in C++20, I doubt the standards committee considers this language gap a "nit".

There are properties of C++, like textual inclusion, that make shipping and maintaining code hard. Piling up header files and expecting downstream users to understand how the include search path will work is the current approach. I would just label it realistic more than a best practice. If it's a best practice for packaging, that is only because the current options are so disatisfying.

And all of that is basically orthogonal to whether you use a language specific package manager or an OS specific one.


> Considering modules are a major feature in C++20, I doubt the standards committee considers this language gap a "nit".

C++20. __Twenty__, man! In the third decade of the language existing.

But I agree that it helps OS package managers.


Different projects have different needs. My current game project requires complex game code. I don't want to write complex code in Lua, but I also don't want to kill my productivity with raw C++. Therefore, I prototype and experiment with a scripting language that fits my needs.

In my case, it is Wren. Some people prefer Chai, AngelScript, Lua, mruby, etc.


Very interesting, can you outline your thought process on why you did not want to write complex code in lua? It's very stable and fast, and has many language constructs etc? It would be completely understandable if your answer is just "this other thing looked cool and I wanted to try it", which is everyone's own choice, but I am wondering if there were any technical reasons for that decision?


I can't talk about the others, so I'll compare Wren and Lua. I dislike Lua's approach to objects, feels clunky and prototype-ish. I also dislike the local/global stuff.

Wren has exception-like error handling and concurrency. And the object model is very similar to C#.


I used to develop a game engine that used an embedded version of Visual Studio to allow C/C++ scripting which compiled into dlls and were hot loaded with any change. And if I recall correctly, that is exactly how the feature film compositor Shake worked: their shader language and entire node graph system is C macros and templates to disguise the fact that it's really C++, and one's compositor projects are really disguised visual studio projects.



Yep. I write my engine in D, which is nicer than c++ for prototyping, but regardless I embed a scheme for scripting.


could you tell me more about the D/scheme integration - specifically what scheme you used, and what you had to do to integrate it with D? i spent a couple of days trying to integrate chibi with D, but gave up because it made too much use of preprocessor macros.


I shopped around quite a bit and eventually decided that s7 scheme[1] would be the best for me. It's been fairly painless to integrate so far, I've just copy-pasted the function prototypes I've needed, as I've needed them. No macros. Engine is closed-source atm so I can't show code, but it's a pretty straight translation from the c examples.

1: https://ccrma.stanford.edu/software/snd/snd/s7.html


thanks, that looks great!


JS and Python are slow [see below] and heavyweight. Lua might work, I don't know... but this being header-only makes it pretty nice for simpler situations.

EDIT: Sorry, I wrote this a little too quickly and ended up writing a bit too coarse of a description. I meant JS and Python are both heavyweight to include in a project, and Python is slow both to initialize and to run. I didn't mean JS is slow to run. I don't know how fast it starts up either.


It is ridiculous that header-only is considered an advantage. The state of C++ build tools is very poor, and it is harming the language as a whole.


> It is ridiculous that header-only is considered an advantage.

Why do you believe it's ridiculous? Being able to integrate a third-party library by just adding a few source files to your source tree is as simple as it gets.

> The state of C++ build tools is very poor, and it is harming the language as a whole.

This assertion makes no sense, particularly in the light of this discussion. Installing a headers-only library is a solved problem, and even template-heavy libraries such as Eigen are already distributed and installed quite easily with standard linux package managers.


I should clarify. Being able to add headers to a project in C++ is easy but adding translation units is not (usually). This encourages header-only libraries even when they are not really appropriate, increasing compilation times etc.


Other things that become a huge albatross and are thus avoided:

- Adding compile-time build steps, e.g. for code generation.

- Adding dependencies of your own, even on, say, a tiny library of helper functions – unless you want to copy and paste it into your header.

With a package manager, those things 'just work'. Package managers also make it much easier to update to newer versions of the library as they're released.


I don't see your point. Adding custom build steps is a basic feature that's supported by pretty much every single popular build system for decades now, just like adding your own dependencies. Heck, cmake even allows users to configure a project to download packages from the web and integrate them in a build and with custom build steps if needed. Even if we ignore this fact, there are also package management tools such as Conan which handle this case quite nicely and also support cross-platform deployments.

And let's not pretend that in some platforms such as pretty much each and every single popular linux distro already package and distribute C++ libraries and offer packaging tools and also package repository services to distribute any dependency.

I'm starting to suspet that those who complain about these sort of issues have little to no experience with C++.


> [cmake stuff]

If that were sufficient, we wouldn't see so many header-only libraries.

> Even if we ignore this fact, there are also package management tools such as Conan which handle thjs case quite nicely and also supporting cross-platform deployments.

Conan would be a decent solution if everyone used it. I tried it briefly last year; I ended up not using it for that project because it didn't support certain features I wanted (namely, compiler toolchain management), plus Bintray was having issues at the time. But my overall impression was that it was... fine. I may end up using it in the future.

For now, though, as a library author, most of your users won't have Conan set up; as a library consumer, most of the libraries you use won't be on Conan; and in either position, most people building your software won't know how to use Conan. Until that changes, it doesn't really solve the library friction problem.

> And let's not pretend that in some platforms such as pretty much each and every single popular linux distro already package and distribute C++ libraries and offer packaging tools and also package repository services to distribute any dependency.

If you have a library which is popular or a dependency for something that's popular, then yes, the N different Linux distributions and package managers for other operating systems will all handle packaging your library themselves. If you just want to be able to upload some code and have it be immediately reusable by the wider world, well, it's not particularly feasible to make packages for N different distributions yourself, and even if you do, the Linux distros most people use are months to years behind the bleeding edge of software releases, so you'll have a while to wait before those packages actually reach people.


Sometimes I wonder why projects are either "single header only" or tens to hundreds of separate modules. One header and one source module would be a nice compromise regarding compile time vs. ease of use.

Yes, I realize that some header only libraries support using the header as either header or implementation, but this still blows up compile time.


Does it really blow up compile time? Translation units and optimization are usually where compile time is spent. Having fewer but fatter translation units helps compile times tremendously while most of the time per translation unit is spent in stages beyond the source compiling in LLVM.

Boost is really the only example I can think of where small utility comes at the expense of huge compile time increases.


Header-only libraries aren't generally single-header-only. That's a bit of an extreme.


> I should clarify. Being able to add headers to a project in C++ is easy but adding translation units is not (usually).

Where do you see a difference?

>This encourages header-only libraries even when they are not really appropriate, increasing compilation times etc.

Thus assertion makes no sense. Headers only declare interfaces, and you only require headers-only libraries if you're deep in template and template metaprogramming land. Evenso it's quite trivial to package and distribute those libraries just like any other library


It's not an absolute advantage, it's just an advantage for some use cases. I certainly wouldn't mind a better build system that didn't make it less advantageous, but it's a hard problem...


It's the state of OS packaging that's very poor. It's not standardized across Android/IOS/Linux/Windows/etc and never will be.

If you stick to Linux and Debian derivatives for example then you've got all the tools for dependencies, building and packaging.


Here, have a small embeddable Javascript: https://bellard.org/quickjs/


Edit: Turns out I saw this on HN earlier. It has a huge problem: it's not portable C!! Have fun with the __attribute__, unistd.h, __builtin_clz, etc.


I'm not sure about JS being necessarily that slow... The browser engines have done tremendous work in the last decade to make it very performant. It approaches java in some regards... It would be interesting to even see applications where this difference would be noticeable, let alone application examples where there would need to be so much complex logic in scripting itself, and also so much sheer computation requirements where this would ever make any difference...

Also then, if this would be the selling point of ChaiScript, I would want to see some benchmarks (I have a hard time believing it will outperform lua with the built-in JIT or one of the faster JS engines with optimizations (it's not like performance is a novel desire in embeddable scripting languages, tons of manhours have been put into them already))... The website doesn't seem to have any such benchmarks.


See my edit, I think I was a little unclear with what precisely each adjective was referring to...


Alright, makes some sense and explains why they are doubling down on this "header only" thing.


How do you mean, python is slow to start up?

    $ time python -c 'exit()'
    python -c 'exit()'  0.01s user 0.01s system 98% cpu 0.021 total
Seems pretty fast to me...

And though python is no speed daemon, for a scripting/embedded language, it's likely to be fast enough.


10ms is already ridiculous for a program that just call exit().

Now try importing some stuff, and also testing a little more thoroughly...

  > python3 -c "import sys, timeit; n = 10; print('%.2f ms/spawn' % (1000 * timeit.timeit('subprocess.call([%r, %r, %r], stdin=subprocess.DEVNULL)' % (sys.executable, '-c', 'import re, argparse, subprocess'), 'import subprocess', number=n) / n))"
  Ubuntu (native): 20.82 ms/spawn
  Ubuntu (WSL):    43.16 ms/spawn
  Windows:         89.45 ms/spawn (without imports: 74 ms/spawn)
For reference on Windows, here's PHP, taking half the time to start up:

  > python3 -c "import sys, timeit; n = 10; print('%.2f ms/spawn' % (1000 * timeit.timeit('subprocess.call([%r, %r, %r], stdin=subprocess.DEVNULL)' % ('php', '-R', ''), 'import subprocess', number=n) / n))"
  45.14 ms/spawn
Now consider that you'll generally be doing a LOT more on initialization than just importing these packages, a lot of packages end up being extremely heavyweight, and very likely you'll be calling Python several times in a row... it can easily waste seconds on startup, even on Linux.


First off, WSL and MSYS2's startup time isn't really fair to compare, since if you're running on windows you're probably going to be running native python. Especially if you're using the kind of application that has scripts/plug-ins. I don't currently have a windows machine to test that, but I imagine you do since you just timed it.

I also don't agree that 10ms is ridiculous. That's barely any time at all. That's less than a frame, at 60Hz. And, how many interpreter instances are you going to start up? Surely, no more instances than you have cores; that means, worst-case scenario if you have a ryzen threadripper or a maxed-out POWER9 (both have 96 cores), a second. For most people, it's going to take under half a second; considering most programs take tens of seconds to start up, I think that's fine.

Not to mention that if you have 'complex initialization code', that's going to take a long time regardless of language; what we're testing the the language core baseline startup time.


WSL is fair, MSYS2 isn't. That's why I took out MSYS2 as I was revising my comment. The timings I have now are just WSL and plain, native Windows.

And the number of interpreters you spawn isn't a function of cores either. It depends entirely what you're doing, and nobody was talking about parallel execution. If you run a bunch of Python scripts in sequence you'll multiply the startup overhead...

I guess we'll just have to disagree on 10ms being ridiculous for a program that does nothing. To me it is. And half a second is orders of magnitude more so.

P.S. All of this is my timing on a pretty darn fast CPU. Try running on a more typical laptop CPU, switching to battery, and you'll get even worse timing.


If you're running a bunch of scripts in sequence, you should have a single interpreter state you use for running all of them. I would do that even if startup were effectively free.

And yeah, I have only a crappy laptop and your benchmark takes more like 50ms/spawn for me. Which I still think is reasonable because there's no reason to make a new interpreter state for ever script.


Your question was "how is Python slow?" where you were apparently arguing over the slowness of standalone Python.

For an embedded application, of course you'd try to keep the interpreter state if it's possible. Sometimes you can't because of global state changes; sometimes you can. And sometimes, even with a single interpreter, the rest of the C++ program is fast enough that the embedded Python initialization time would dominate its running time, rendering the standalone vs. embedded distinction moot.

In any case... all you're doing is amortizing slow startup cost over the lifetime of the program. The startup is still slow. You were wondering "how" the startup is slow, so I wrote you benchmarks and ran them to illustrate. If what you really wanted to argue was that startup time is irrelevant, maybe you should say that instead of asking "how is startup slow" and making me spend my time writing a benchmark for something you don't care about.


By the by, why does your python example import a bunch of things, but the php code imports nothing? That would negate the entire purpose of the experiment, no...


Because, there wasn't anything to import?! preg_match(), getopt(), system(), etc. are already available... if anything, you should be asking why I'm unfairly penalizing PHP by comparing it against a no-import Python!

And I did give you an example of Python that doesn't import anything, and as you can see (and could have easily tested yourself) it was still a lot slower...


Python not importing anything is the same speed as php not importing anything on my system. Oh, and--incidentally, javascript is 20% faster than php and python both, and afaik you don't have to import libraries there either.


Yes, so that means Python is slower to start on your system too. Not sure what you mean by "Javascript"... what program are you running?


Javascriptcore; jsc. It's part of webkit (comes from the webkit2gtk package on my system).


Oh I see. Cool.

Now compare with Lua:

  $ python3 -c "import sys, timeit; n = 10; print('%.2f ms/spawn' % (1000 * timeit.timeit('subprocess.call([%r, %r], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL)' % ('lua', '-'), 'import subprocess', number=n) / n))"
  1.09 ms/spawn
NewLisp:

  $ python3 -c "import sys, timeit; n = 10; print('%.2f ms/spawn' % (1000 * timeit.timeit('subprocess.call([%r, %r], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL)' % ('newlisp', '-'), 'import subprocess', number=n) / n))"
  1.25 ms/spawn


    python3 -S
To bypass unnecessary startup stuff.


I'm indeed aware, but it doesn't change the conclusion, and I was trying to keep things consistent with the parent comment. All that does is reduce 89ms to 55ms.


That's a significant improvement. Believe latest versions have been working on this number as well.


Not saying it's insignificant, just saying it doesn't change the conclusion.


I just have to upvote this since Jason Turner is one of the creators and I owe a ton of my knowledge about C++ to his videos on his YT channel, as well as his talks at CppCon.


He's also co-host of the podcast CppCast :) https://cppcast.com/


I really like ChaiScript and have used it in a couple of small game projects.

But I stopped using it because it was insanely slow (in both compile times and runtime performance). Several hundred (if not thousand) times slower than the equivalent Lua code.

That was a few years ago, but skimming through the release notes, it doesn't seem to have been fixed.


I bet the compile times would improve if it wasn't header only.


Unfortunately ChaiScript needs to be header-only because it's like 99% templates.

You can put all your ChaiScript-related code into an isolated implementation file, and try to avoid touching that file as much as possible. But that only works well for long-term projects, where once you get all the bindings you need set up, you can coast for a while without touching those files.

For short-term projects it's a nightmare because you're constantly touching the ChaiScript files.

There's no inherent reason a header-only library should significantly impact compile times, aside from the fact that the authors usually don't have the foresight to make it efficient. It's also very difficult to find information about how to make templates more efficient at compile time.


> I bet the compile times would improve if it wasn't header only.

If any hypothetical compilation time problem concerns you then rest assure that C++ enables you to develop submodules that wrap and instantiate your templates, eliminating the need to recompile them every single build.


For the note: ChaiScript is from Jason Turner. He also does CPPCast ( https://www.youtube.com/channel/UCuCjADS4u3uJDTqUaG0H9dA) and is a regular presenter at CPPCon ( https://www.youtube.com/watch?v=zBkNBP00wJE ).

Many of his videos / tutorials are worth a look if you are interested in C++.


Wow this looks awesome, great job! I didn't even know I wanted this until I saw it.

I am worried though how memory management is handled under the hood. Is it possible to use traditional C++ memory management with any of the variables created in ChaiScript?

I'm not saying that I don't trust the implementation, I'd just like more control over it in some cases.


Looks very nice! It would be helpful to have a page that compares it to some of the other popular scripting languages for C++.


I like this and I see the use cases, however why would I use ChaiScript over Lua for example?


Thread aware, exceptions, traditional OO. Guessing because it has features that might line up better with c++?


To me it looks like ChaiScript is more lightweight and easier to learn.


Argueably a questionable advantage over for example lua specifically, where it is already quite easy, while at the same time having all the benefits of a mature production grade system: speed, stability, portability, talent pool, etc. Even Python could be a contester here...


If it's closer to C++ in its syntax I think it's more interesting.


Would that make it a good candidate for microcontroller programming? I have been looking for something more elegant/functional to program arduino with. Anything about memory size on the site?


"ChaiScript is a header only library with only one dependency: The operating system provided dynamic library loader, which has to be specified on some platforms."

I believe the dynamic library loader requirement is a showstopper for AVR.


Not exactly arduino, but you may be interested with nodemcu: https://en.wikipedia.org/wiki/NodeMCU


> ChaiScript has been making stable releases since 2009. No bugfix or feature is made without a corresponding unit test

Codecov: 72%

Reality is often disappointing...

In all seriousness, looks pretty nice.


Since when code coverage is accurate enough for you to make that judgement?


Clearly I made the mistake of trying to tell a joke in this sacred forum.

72% is more than high enough for me to believe them, but it's not 100% hence the gag


...Yeah, your joke isn't really successful... I didn't realize it is a joke either.


Ha! Chai == Tea in Hindi, for those who are wondering ;)


chai is tea in many languages that either inherit from Russian or Arabic

https://www.etymonline.com/word/chai

Edit: apparently the original source is China. Etymonline fail!


With few exceptions, the word for Tea in a country depends on how it was originally transported there.

See: Tea if by sea, cha if by land: Why the world only has two words for tea

https://qz.com/1176962/map-how-the-word-tea-spread-over-land...


Wow. I had absolutely no idea! Thanks for sharing that link, learned something new today


I thought Chai originally comes from the Chinese word 茶 (Chá).


Correct -- the etymology of all "chai"s and "tea"s can be traced to Chinese:

https://en.wikipedia.org/wiki/Etymology_of_tea


LuaJIT w/ FFI seems better imo.


I just discovered that raw multi line string don't work on visual studio 2017...


With the paratheses? R”(str)”


Oh right braces are needed! Thanks!


I'm stupid but what's the purpose? Why not just call the function directly without Chai?


Calling a C++ function from your scripting language, or a scripted function from C++ are different uses, they're just combined into one for the purposes of a demo of what the code looks like.

edit: For example, there's a fairly clear distinction in a game between engine code and gameplay code. Engine code is run all the time, and should be memory and computationally efficient, and robust. It won't likely need to be changed much, and is OK to spend some extra effort writing it to get all these properties. So C++ is great for this.

Gameplay code doesn't tend to have these requirements, and you may not want to pay the cost to write C++ if you don't need the benefits. And additionally, gameplay code often needs to be rewritten frequently while you experiment with gameplay to find what's fun, or do expansions, or support mods to the game, so a language that's very easy to write in helps a lot.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: