Show HN: A small virtual machine written in C
github.com
github.com
#define binop(NAME, OP) definstr (NAME) { \
long long b, a; \
if (carp_stack_pop(&m->stack, &b) == -1)\
carp_vm_err(m, CARP_STACK_EMPTY);\
if (carp_stack_pop(&m->stack, &a) == -1)\
carp_vm_err(m, CARP_STACK_EMPTY);\
carp_stack_push(&m->stack, a OP b);}
binop(ADD,+)
binop(MUL,*)
...
The repetition of if (carp_stack_pop(&m->stack, &a) == -1)
carp_vm_err(m, CARP_STACK_EMPTY);
seems like a good place to just use a function to encapsulate all the error checking and handling.[1] https://github.com/tekknolagi/carp/blob/master/src/carp_inst...
[2] http://www.embedded.com/design/programming-languages-and-too... and the following parts
GPL?
My personal preference for licensing is one of the BSD or MIT licenses, I'd rather see things of mine used to makie the world a better place than force people to share what they've done with it.
Edit: Just kidding. That was dumb.
[1] https://news.ycombinator.com/item?id=3951575
[2] https://news.ycombinator.com/item?id=3951480
[3] http://www.theverge.com/android/2012/5/9/3010404/judge-denie...
https://sites.google.com/site/lccretargetablecompiler/
The code in that book feels pretty dated in a lot of places, but this was a neat trick. Every piece of information related to a token is defined in one place:
https://github.com/drh/lcc/blob/master/src/token.h
Then here's how it's used:
For example, instead of using registers, why not have OR pick values off the stack like ADD, or vice versa? Why use EAX for the conditional jump instructions when you could look at the top of the stack? Why have REM when you can just MOV from ERX?
If you feel the urge to convert the VM to a JIT, rather than calling function pointers for each instruction, you might find this blog post useful: http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-s...
Not sure I understand. What would it do differently?
Edit: Hang on, so I should generate C or asm? or something?
It's nice having a normal, interpreted VM then building a JIT compiler later on for extra speed. A recent Rust project called `js.rs`[0] started out with a simple interpreter and swapped it for a JIT compiler using libjit.
The biggest difference being you now have to interface with the actual CPU and such.
I only added the blog link in case you were interested in extending the VM to use Just In Time compiling. Using a JIT would mean generating assembly.
I like your VM - there is no need to change it. Thanks for sharing.
[1] http://realityforge.org/code/virtual-machines/2011/05/19/int...
I wrote a 0-operand VM in C a few years ago that used a lot of the same concepts but considerably more code, like 10x at least. I will learn a lot from this.
What is a 0 operand VM? I'd love to see it!
also, OP, your code looks great.
Thanks! It's my first big C project that isn't hideous :D
What I mean by missing files is that it's so clean that at first glance it didn't look like the whole deal :)
This is pretty cool!
Otherwise fun project.
[0]: http://www.amazon.com/Programming-Modern-Approach-2nd-Editio...
$ git clone https://github.com/tekknolagi/carp
$ make
$ ./carp
$ ./carp --help # after peeking in carp.c
help msg
There's a tantalizing target called 'tests' in the Makefile, but it doesn't work.There's some examples, but no makefile to build them. How do you run them?
I see you used to have some .carp files but you just deleted them: https://github.com/tekknolagi/carp/commit/fa16eeb443
I tried restoring one, but it doesn't work:
$ git checkout fa16eeb443~1 examples/reg.carp
$ ./carp -f examples/reg.carp
Unknown label <add>
So at this point I'm ready to give up..You run the examples like so: carp -f examples/carp/call.carp
You can compile C files that use the Carp API, but I have not written any documentation yet.
$ ./carp -f examples/carp/call.carp
Unknown label <add>
(I am very interested in building easy-to-understand codebases: http://akkartik.name/post/wart)Try rebuilding carp? o.O
That was some very nice documentation!
Edit: I just noticed you changed the target (why?) and are also immediately deleting carp.out. So I was indeed using the stale version in spite of using 'make clean' :)
Anyways, so I did this, but still no luck.
$ git diff
diff --git a/Makefile b/Makefile
index f921d96..7c55b4e 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,6 @@ all:
$(CC) $(CFLAGS) $(SRCS)
ar cr libcarp.a $(OBJS)
$(CC) src/carp.c libcarp.a -o $(PROG)
- make clean_objs
#.PHONY: tests
$ make
gcc -c -std=c99 -Wall -Werror -Wno-unused-variable -Wno-format-security -O3 -static src/carp_instructions.c src/carp_lexer.c src/carp_machine.c src/carp_tokenizer.c src/lib/carp_stack.c src/lib/carp_ht.c
ar cr libcarp.a *.o
gcc src/carp.c libcarp.a -o carp.out
$ ./carp.out -f reg.carp
Unknown label <add> $ git diff
diff --git a/Makefile b/Makefile
index 1263dd8..6592c59 100644
--- a/Makefile
+++ b/Makefile
@@ -1,16 +1,16 @@
CC = gcc #/usr/local/bin/gcc-4.2
PREFIX = /usr/local
NDEBUG ?=
-CFLAGS = -c -std=c99 -Wall -Werror -Wno-unused-variable -Wno-format-security -O3 -static $(NDEBUG)
+CFLAGS = -std=c99 -Wall -Werror -Wno-unused-variable -Wno-format-security -O3 -static $(NDEBUG)
SRCS = src/carp_instructions.c src/carp_lexer.c src/carp_machine.c src/carp_tokenizer.c src/lib/carp_stack.c src/lib/carp_ht.c
#$(wildcard src/*.c src/lib/*.c)
OBJS = *.o
PROG = carp.out
all:
- $(CC) $(CFLAGS) $(SRCS)
+ $(CC) -c $(CFLAGS) $(SRCS)
ar cr libcarp.a $(OBJS)
- $(CC) src/carp.c libcarp.a -o $(PROG)
+ $(CC) $(CFLAGS) src/carp.c libcarp.a -o $(PROG)
#.PHONY: tests
Still, I'm concerned that you might be triggering some sort of undefined behavior, which means it might work for you but not on a slightly different machine or compiler version. I tried printing out token lexemes in carp_run_program, right after the call to tokenize(), and some of the tokens printed binary garbage, suggesting that they might be missing a terminating null, or worse. Does this look right? $ git diff
...
diff --git a/src/carp.c b/src/carp.c
index 9268d98..19e16d9 100644
--- a/src/carp.c
+++ b/src/carp.c
@@ -88,6 +88,8 @@ void carp_print_conditions () {
void carp_run_program (char *fn) {
carp_tok *tokens = carp_lex_tokenize(fn);
+ for (carp_tok* tt = tokens; tt; tt=tt->next)
+ printf("%s %d\n", tt->lexeme, tt->type);
if (tokens == NULL) {
fprintf(stderr, "Something went wrong with tokenization.\n");
exit(1);
$ make && ./carp.out -f ./examples/carp/call.carp
gcc -c -std=c99 -Wall -Werror -Wno-unused-variable -Wno-format-security -O3 -static src/carp_instructions.c src/carp_lexer.c src/carp_machine.c src/carp_tokenizer.c src/lib/carp_stack.c src/lib/carp_ht.c
ar cr libcarp.a *.o
gcc -std=c99 -Wall -Werror -Wno-unused-variable -Wno-format-security -O3 -static src/carp.c libcarp.a -o carp.out
add 3
gload 6
-5l 1
gload 6
-4l 1
add 6
ret 6
main 3
push 6
7�l 1
push 6
9�l 1
call 6
add 4
2 1
ptop 6
halt 6
0 1
16
The trouble with C/C++ is that it's so easy to end up with undefined behavior :/Also, yeah your code looks fine; I added it in and it worked fine. That's unfortunate. I'll debug soon.
(Perhaps we should take this offline. My email is in my profile.)
Check out http://github.com/tekknolagi/gecho — shudder
The author explicitly mentions, he is in the process of learning C while he's building something using it.
This is interesting because the folks at [osdev.org](http://wiki.osdev.org) keep stressing that you must be a god-level expert in C, before you even think of getting into systems programming.
Georgia Tech offers a class where you code a VM for the LC3b instruction set (http://users.ece.gatech.edu/~moin/s13a/hw.html).
It was a great learning experience, and pretty fun too.
Then I read this: http://stackoverflow.com/questions/2034422/tutorial-resource...
and this: http://courses.cms.caltech.edu/cs11/material/c/mike/lab8/lab...
and took a look at the MSP430's instruction set.
As far as where they can be used... anywhere? I don't know the answer to your question - sorry :)
I wonder in a general sense if there are many VMs that explicitly copy good older CPUs. I've always thought the 6809 would make an awesome VM. After all, it was pretty awesome to code on in the real world. Or a VM based on the classic 1802, 6502, or Z80 with some minor mods.
If you really want to warp peoples minds give them a VM based on IBM HAL assembler, basically turn Hercules and VM into a hypervisor rather than an application.
A PDP-11 inspired/based virtual machine. Hmm.
But if you want performance you'd rather tailor your instruction set to the language it will execute (assuming you're not making a general purpose VM). CPU makers like Intel do actually keep a close look on what programs are doing in order to evolve their instruction set.
the power of function pointers for stuff like this is quite enormous. making a vm like this is great practice for a real compiler too... :)
i'm curious though, why it isn't a direct copy of x86? other than the obvious 'thats really complicated' it would save a lot of wheel reinventing...
EDIT: Btw, do you happen to have a book or some study material on this subject ? Anything you can recommend ?
I don't - I wasn't really following anything unified. I just pulled together a bunch of crappy web resources :)