Signed-off-by: Tomek Grabiec <[email protected]>
---
arch/x86/Makefile_32 | 1 +
arch/x86/emit-code_32.c | 41 ++++++++++++---
arch/x86/exception_32.c | 10 +++-
arch/x86/include/arch/emit-code.h | 3 +-
arch/x86/include/arch/exception.h | 6 ++-
arch/x86/include/arch/stack-frame.h | 12 ++++
arch/x86/insn-selector_32.brg | 2 +
arch/x86/stack-frame.c | 27 ++++++++++
arch/x86/unwind_32.S | 73 ++++++++++++++++++++++++++
include/jit/compilation-unit.h | 2 +
include/jit/exception.h | 8 +++
jit/compilation-unit.c | 11 ++++
jit/emit.c | 3 +
jit/exception.c | 98 +++++++++++++++++++++++++++++++++++
scripts/build/test.mk | 4 ++
test/arch-mmix/stack-frame.c | 32 +++++++++++
test/arch-x86/Makefile | 5 ++
test/include/arch/exception.h | 10 ++++
test/include/arch/stack-frame.h | 8 +++
test/jamvm/thread-stub.c | 8 +++
test/jit/Makefile | 3 +
21 files changed, 356 insertions(+), 11 deletions(-)
create mode 100644 arch/x86/unwind_32.S
create mode 100644 test/arch-mmix/stack-frame.c
create mode 100644 test/include/arch/exception.h
create mode 100644 test/jamvm/thread-stub.c
diff --git a/arch/x86/Makefile_32 b/arch/x86/Makefile_32
index c65c9f0..81bb5ad 100644
--- a/arch/x86/Makefile_32
+++ b/arch/x86/Makefile_32
@@ -8,6 +8,7 @@ ARCH_OBJS = \
arch/x86/stack-frame.o \
arch/x86/use-def.o \
arch/x86/exception_32.o \
+ arch/x86/unwind_32.o \
jamvm/os/$(OS)/i386/dll_md.o \
jamvm/os/$(OS)/i386/init.o \
jamvm/os/$(OS)/os.o
diff --git a/arch/x86/emit-code_32.c b/arch/x86/emit-code_32.c
index 97cc3fb..43ae427 100644
--- a/arch/x86/emit-code_32.c
+++ b/arch/x86/emit-code_32.c
@@ -11,6 +11,8 @@
#include <jit/statement.h>
#include <jit/compilation-unit.h>
#include <jit/compiler.h>
+#include <jit/exception.h>
+#include <jit/stack-slot.h>
#include <vm/list.h>
#include <vm/buffer.h>
@@ -19,6 +21,7 @@
#include <arch/emit-code.h>
#include <arch/instruction.h>
#include <arch/memory.h>
+#include <arch/exception.h>
#include <assert.h>
#include <errno.h>
@@ -27,6 +30,12 @@
#include <stdbool.h>
#include <string.h>
+#define PREFIX_SIZE 1
+#define BRANCH_INSN_SIZE 5
+#define BRANCH_TARGET_OFFSET 1
+
+#define CALL_INSN_SIZE 5
+
/*
* __encode_reg: Encode register to be used in IA-32 instruction.
* @reg: Register to encode.
@@ -431,8 +440,6 @@ static void emit_push_imm(struct buffer *buf, struct
operand *operand)
__emit_push_imm(buf, operand->imm);
}
-#define CALL_INSN_SIZE 5
-
static void __emit_call(struct buffer *buf, void *call_target)
{
int disp = call_target - buffer_current(buf) - CALL_INSN_SIZE;
@@ -451,7 +458,11 @@ void emit_ret(struct buffer *buf)
emit(buf, 0xc3);
}
-void emit_epilog(struct buffer *buf, unsigned long nr_locals)
+/*
+ * Emitted code must not write to ECX register because it may hold
+ * exception object reference when in unwind block
+ */
+static void __emit_epilog(struct buffer *buf, unsigned long nr_locals)
{
if (nr_locals)
emit(buf, 0xc9);
@@ -462,10 +473,30 @@ void emit_epilog(struct buffer *buf, unsigned long
nr_locals)
__emit_pop_reg(buf, REG_EBX);
__emit_pop_reg(buf, REG_ESI);
__emit_pop_reg(buf, REG_EDI);
+}
+void emit_epilog(struct buffer *buf, unsigned long nr_locals)
+{
+ __emit_epilog(buf, nr_locals);
emit_ret(buf);
}
+static void __emit_jmp(struct buffer *buf, unsigned long addr)
+{
+ unsigned long current = (unsigned long)buffer_current(buf);
+ emit(buf, 0xE9);
+ emit_imm32(buf, addr - current - BRANCH_INSN_SIZE);
+}
+
+void emit_unwind(struct buffer *buf, unsigned long nr_locals)
+{
+ /* save exception object in ECX */
+ __emit_pop_reg(buf, REG_ECX);
+
+ __emit_epilog(buf, nr_locals);
+ __emit_jmp(buf, (unsigned long)&unwind);
+}
+
static void emit_adc_reg_reg(struct buffer *buf,
struct operand *src, struct operand *dest)
{
@@ -681,10 +712,6 @@ void emit_branch_rel(struct buffer *buf, unsigned char
prefix,
emit_imm32(buf, rel32);
}
-#define PREFIX_SIZE 1
-#define BRANCH_INSN_SIZE 5
-#define BRANCH_TARGET_OFFSET 1
-
static long branch_rel_addr(struct insn *insn, unsigned long target_offset)
{
long ret;
diff --git a/arch/x86/exception_32.c b/arch/x86/exception_32.c
index c703859..22e8028 100644
--- a/arch/x86/exception_32.c
+++ b/arch/x86/exception_32.c
@@ -24,9 +24,15 @@
* Please refer to the file LICENSE for details.
*/
+#include <jit/exception.h>
#include <arch/exception.h>
+#include <arch/stack-frame.h>
-unsigned char *throw_exception(struct object *exception)
+unsigned char *throw_exception(struct compilation_unit *cu,
+ struct object *exception)
{
- return NULL; /* TODO */
+ unsigned char *native_ptr = __builtin_return_address(0) - 1;
+ struct jit_stack_frame *frame = __builtin_frame_address(1);
+
+ return throw_exception_from(cu, frame, native_ptr, exception);
}
diff --git a/arch/x86/include/arch/emit-code.h
b/arch/x86/include/arch/emit-code.h
index 52d1ef4..5ae5d93 100644
--- a/arch/x86/include/arch/emit-code.h
+++ b/arch/x86/include/arch/emit-code.h
@@ -10,5 +10,6 @@ void emit_prolog(struct buffer *, unsigned long);
void emit_epilog(struct buffer *, unsigned long);
void emit_body(struct basic_block *, struct buffer *);
void emit_trampoline(struct compilation_unit *, void *, struct
jit_trampoline*);
-
+void emit_unwind(struct buffer *, unsigned long);
+
#endif /* __X86_EMIT_CODE */
diff --git a/arch/x86/include/arch/exception.h
b/arch/x86/include/arch/exception.h
index 422321e..1b52e6e 100644
--- a/arch/x86/include/arch/exception.h
+++ b/arch/x86/include/arch/exception.h
@@ -3,7 +3,11 @@
#include <vm/vm.h>
+struct compilation_unit;
+
/* This should be called only by JIT compiled native code */
-unsigned char *throw_exception(struct object *exception);
+unsigned char *throw_exception(struct compilation_unit *cu,
+ struct object *exception);
+extern void unwind();
#endif
diff --git a/arch/x86/include/arch/stack-frame.h
b/arch/x86/include/arch/stack-frame.h
index ec1338a..1bb9e0f 100644
--- a/arch/x86/include/arch/stack-frame.h
+++ b/arch/x86/include/arch/stack-frame.h
@@ -5,9 +5,21 @@
struct methodblock;
struct expression;
+struct compilation_unit;
+
+struct jit_stack_frame {
+ struct jit_stack_frame *prev;
+ unsigned long old_ebx;
+ unsigned long old_esi;
+ unsigned long old_edi;
+ unsigned long return_address;
+ unsigned long args[0];
+} __attribute__((packed));
unsigned long frame_local_offset(struct methodblock *, struct expression *);
unsigned long slot_offset(struct stack_slot *slot);
unsigned long frame_locals_size(struct stack_frame *frame);
+bool is_jit_method(unsigned long eip);
+unsigned long cu_frame_locals_offset(struct compilation_unit *cu);
#endif
diff --git a/arch/x86/insn-selector_32.brg b/arch/x86/insn-selector_32.brg
index c4d24ef..3bf0b6d 100644
--- a/arch/x86/insn-selector_32.brg
+++ b/arch/x86/insn-selector_32.brg
@@ -1105,7 +1105,9 @@ stmt: STMT_ATHROW(reg)
struct var_info *reg_eax = get_fixed_var(s->b_parent, REG_EAX);
select_insn(s, tree, reg_insn(INSN_PUSH_REG, state->left->reg1));
+ select_insn(s, tree, imm_insn(INSN_PUSH_IMM, (unsigned
long)s->b_parent));
select_insn(s, tree, rel_insn(INSN_CALL_REL, (unsigned
long)throw_exception));
+ method_args_cleanup(s, tree, 1);
/* Jump where throw_exception() told us to jump */
select_insn(s, tree, reg_insn(INSN_PUSH_REG, reg_eax));
diff --git a/arch/x86/stack-frame.c b/arch/x86/stack-frame.c
index cf1a612..b769932 100644
--- a/arch/x86/stack-frame.c
+++ b/arch/x86/stack-frame.c
@@ -25,6 +25,7 @@
*/
#include <jit/expression.h>
+#include <jit/compilation-unit.h>
#include <vm/vm.h>
#include <arch/stack-frame.h>
#include <stdlib.h>
@@ -90,3 +91,29 @@ unsigned long frame_locals_size(struct stack_frame *frame)
unsigned long nr_locals = frame->nr_local_slots - frame->nr_args;
return __index_to_offset(nr_locals + frame->nr_spill_slots);
}
+
+/* Points to the first address past text segment */
+extern char etext;
+
+/*
+ * Checks whether address belongs to jitted or JATO method.
+ * This is used in deciding when to stop the unwind process upon
+ * exception throwing.
+ *
+ * It utilises the fact, that jitted code is allocated on heap. So by
+ * comparing return address with text segment end we can tell whether
+ * the caller is on heap or in text.
+ */
+bool is_jit_method(unsigned long eip)
+{
+ return eip >= (unsigned long)&etext;
+}
+
+/*
+ * Returns total offset to subtract from ESP to reserve space for locals.
+ */
+unsigned long cu_frame_locals_offset(struct compilation_unit *cu)
+{
+ unsigned long frame_size = frame_locals_size(cu->stack_frame);
+ return frame_size * sizeof(unsigned long);
+}
diff --git a/arch/x86/unwind_32.S b/arch/x86/unwind_32.S
new file mode 100644
index 0000000..a0cb68c
--- /dev/null
+++ b/arch/x86/unwind_32.S
@@ -0,0 +1,73 @@
+.global unwind
+.text
+
+/*
+ * unwind - Performs the following:
+ * 1) Resets the stack pointer to point at the end of
+ * caller's stack frame
+ * 2) Rethrows the exception in caller's context
+ */
+unwind:
+ popl %edx /* method's return address */
+ decl %edx
+
+ /*
+ * Get compilation unit
+ */
+ push %ecx
+ push %edx
+ call get_cu_from_native_addr
+ pop %edx
+ pop %ecx
+
+ /*
+ * Restore stack pointer.
+ * We need to restore space for locals too.
+ */
+ push %ecx
+ push %edx
+
+ push %eax
+ call cu_frame_locals_offset
+ addl $4, %esp
+
+ pop %edx
+ pop %ecx
+
+ movl %ebp, %esp
+ subl %eax, %esp
+
+ /*
+ * Get compilation unit
+ */
+ push %ecx
+ push %edx
+ call get_cu_from_native_addr
+ pop %edx
+ pop %ecx
+
+ pushl %ecx /* push exception object reference to handler */
+
+ /*
+ * throw_exception_from(frame, native_ptr, exception);
+ */
+ pushl %ecx
+ pushl %edx
+ pushl %ebp
+ pushl %eax
+ call throw_exception_from
+ popl %ecx /* cu -> ECX */
+ addl $12, %esp
+
+ test %eax,%eax
+ jnz finish
+
+ /* Jump to exit block. Do not push exception object */
+ add $4, %esp
+ pushl %ecx
+ call cu_exit_bb_native_ptr
+ addl $4, %esp
+
+finish:
+ pushl %eax
+ ret
diff --git a/include/jit/compilation-unit.h b/include/jit/compilation-unit.h
index 9de65d7..2c5e0b3 100644
--- a/include/jit/compilation-unit.h
+++ b/include/jit/compilation-unit.h
@@ -18,6 +18,7 @@ struct compilation_unit {
struct methodblock *method;
struct list_head bb_list;
struct basic_block *exit_bb;
+ struct basic_block *unwind_bb;
struct var_info *var_infos;
unsigned long nr_vregs;
struct buffer *objcode;
@@ -44,6 +45,7 @@ struct basic_block *find_bb(struct compilation_unit *,
unsigned long);
unsigned long nr_bblocks(struct compilation_unit *);
void compute_insn_positions(struct compilation_unit *);
int sort_basic_blocks(struct compilation_unit *);
+unsigned char *cu_exit_bb_native_ptr(struct compilation_unit *);
#define for_each_variable(var, var_list) for (var = var_list; var != NULL; var
= var->next)
diff --git a/include/jit/exception.h b/include/jit/exception.h
index 6ea47f0..dbbc18b 100644
--- a/include/jit/exception.h
+++ b/include/jit/exception.h
@@ -3,6 +3,9 @@
#include <jit/compilation-unit.h>
#include <vm/vm.h>
+#include <arch/stack-frame.h>
+
+struct jit_stack_frame;
struct exception_table_entry *exception_find_entry(struct methodblock *,
unsigned long);
@@ -13,4 +16,9 @@ static inline bool exception_covers(struct
exception_table_entry *eh,
return eh->start_pc <= offset && offset < eh->end_pc;
}
+unsigned char *throw_exception_from(struct compilation_unit *cu,
+ struct jit_stack_frame *frame,
+ unsigned char *native_ptr,
+ struct object *exception);
+
#endif
diff --git a/jit/compilation-unit.c b/jit/compilation-unit.c
index 0848f69..652ea7c 100644
--- a/jit/compilation-unit.c
+++ b/jit/compilation-unit.c
@@ -46,6 +46,11 @@ struct compilation_unit *alloc_compilation_unit(struct
methodblock *method)
cu->exit_bb = alloc_basic_block(cu, 0, 0);
if (!cu->exit_bb)
goto out_of_memory;
+
+ cu->unwind_bb = alloc_basic_block(cu, 0, 0);
+ if (!cu->unwind_bb)
+ goto out_of_memory;
+
pthread_mutex_init(&cu->mutex, NULL);
cu->stack_frame = alloc_stack_frame(method->args_count,
method->max_locals);
@@ -84,6 +89,7 @@ void free_compilation_unit(struct compilation_unit *cu)
pthread_mutex_destroy(&cu->mutex);
free_basic_block(cu->exit_bb);
+ free_basic_block(cu->unwind_bb);
free_buffer(cu->objcode);
free_var_infos(cu->var_infos);
free_stack_frame(cu->stack_frame);
@@ -179,3 +185,8 @@ int sort_basic_blocks(struct compilation_unit *cu)
{
return list_sort(&cu->bb_list, bb_list_compare);
}
+
+unsigned char *cu_exit_bb_native_ptr(struct compilation_unit *cu)
+{
+ return bb_native_ptr(cu->exit_bb);
+}
diff --git a/jit/emit.c b/jit/emit.c
index 1c8af96..22ab463 100644
--- a/jit/emit.c
+++ b/jit/emit.c
@@ -44,6 +44,9 @@ int emit_machine_code(struct compilation_unit *cu)
emit_body(cu->exit_bb, cu->objcode);
emit_epilog(cu->objcode, frame_size);
+ emit_body(cu->unwind_bb, cu->objcode);
+ emit_unwind(cu->objcode, frame_size);
+
return 0;
}
diff --git a/jit/exception.c b/jit/exception.c
index b3d3dd5..2a98dab 100644
--- a/jit/exception.c
+++ b/jit/exception.c
@@ -26,6 +26,10 @@
#include <jit/exception.h>
#include <jit/compilation-unit.h>
+#include <jit/bc-offset-mapping.h>
+#include <vm/buffer.h>
+#include <arch/exception.h>
+#include <arch/stack-frame.h>
struct exception_table_entry *exception_find_entry(struct methodblock *method,
unsigned long target)
@@ -41,3 +45,97 @@ struct exception_table_entry *exception_find_entry(struct
methodblock *method,
return NULL;
}
+
+static unsigned char *eh_native_ptr(struct compilation_unit *cu,
+ struct exception_table_entry *eh)
+{
+ struct basic_block *bb = find_bb(cu, eh->handler_pc);
+
+ assert(bb != NULL);
+
+ return bb_native_ptr(bb);
+}
+
+/**
+ * find_handler - return native pointer to exception handler for given
+ * @exception_class and @bc_offset of source.
+ */
+static unsigned char *find_handler(struct compilation_unit *cu,
+ Class *exception_class,
+ unsigned long bc_offset)
+{
+ struct exception_table_entry *eh;
+ struct methodblock *method = cu->method;
+ int size = method->exception_table_size;
+ int i;
+
+ for (i = 0; i < size; i++) {
+ eh = &method->exception_table[i];
+
+ if (exception_covers(eh, bc_offset)) {
+ Class *catch_class;
+
+ if (eh->catch_type == 0)
+ break; /* It's a finally block */
+
+ catch_class = resolveClass(method->class,
+ eh->catch_type,
+ false);
+
+ if (isInstanceOf(catch_class, exception_class))
+ break;
+ }
+ }
+
+ if (i < size)
+ return eh_native_ptr(cu, eh);
+
+ return NULL;
+}
+
+/**
+ * throw_exception_from - returns native pointer inside jitted method
+ * that sould be executed to handle exception.
+ * This can be one of the following:
+ * 1) registered exception handler (catch/finally block)
+ * 2) method's unwind block (when no handler is found)
+ * 3) method's exit block (when no handler is found and
+ * unwind can't be done because the method's caller
+ * is not a jitted method).
+ *
+ * @frame: frame pointer of method throwing exception
+ * @native_ptr: pointer to instruction that caused exception
+ * @exception: exception object to throw.
+ */
+unsigned char *throw_exception_from(struct compilation_unit *cu,
+ struct jit_stack_frame *frame,
+ unsigned char *native_ptr,
+ struct object *exception)
+{
+ unsigned char *eh_ptr = NULL;
+ unsigned long bc_offset;
+
+ if (getExecEnv()->exception != NULL) {
+ /* Looks like we've caught some asynchronous exception,
+ which must have precedence. */
+ exception = getExecEnv()->exception;
+ getExecEnv()->exception = NULL;
+ }
+
+ bc_offset = native_ptr_to_bytecode_offset(cu, native_ptr);
+ if (bc_offset != BC_OFFSET_UNKNOWN) {
+ eh_ptr = find_handler(cu, exception->class, bc_offset);
+ if (eh_ptr != NULL)
+ return eh_ptr;
+ }
+
+ if (!is_jit_method(frame->return_address)) {
+ /* No handler found within jitted method call
+ chain. Signal exception and return to previous
+ (non-jitted) method. */
+ getExecEnv()->exception = exception;
+ return NULL;
+ }
+
+ return bb_native_ptr(cu->unwind_bb);
+}
diff --git a/scripts/build/test.mk b/scripts/build/test.mk
index e3f1b1a..a4afcdb 100644
--- a/scripts/build/test.mk
+++ b/scripts/build/test.mk
@@ -14,6 +14,10 @@ endif
$(E) " CC " $@
$(Q) $(CC) $(ARCH_CFLAGS) $(DEFAULT_CFLAGS) $(CFLAGS) $(INCLUDE)
$(DEFINES) -c $< -o `basename $...@`
+%.o: %.S
+ $(E) " AS " $@
+ $(Q) $(AS) $(AFLAGS) $< -o `basename $...@`
+
test: $(RUNNER)
$(RUNNER): $(SUITE) $(OBJS)
diff --git a/test/arch-mmix/stack-frame.c b/test/arch-mmix/stack-frame.c
new file mode 100644
index 0000000..78021db
--- /dev/null
+++ b/test/arch-mmix/stack-frame.c
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2009 Tomasz Grabiec
+ *
+ * This file is released under the GPL version 2 with the following
+ * clarification and special exception:
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under terms
+ * of your choice, provided that you also meet, for each linked independent
+ * module, the terms and conditions of the license of that module. An
+ * independent module is a module which is not derived from or based on
+ * this library. If you modify this library, you may extend this exception
+ * to your version of the library, but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version.
+ *
+ * Please refer to the file LICENSE for details.
+ */
+
+#include <arch/stack-frame.h>
+
+bool is_jit_method(unsigned long eip)
+{
+ return false;
+}
diff --git a/test/arch-x86/Makefile b/test/arch-x86/Makefile
index 8e3b6ad..c8b4227 100644
--- a/test/arch-x86/Makefile
+++ b/test/arch-x86/Makefile
@@ -24,6 +24,8 @@ OBJS = \
../jamvm/alloc-stub.o \
../jamvm/cast-stub.o \
../jamvm/lock.o \
+ ../jamvm/thread-stub.o \
+ ../jamvm/resolve-stub.o \
../../arch/x86/backtrace.o \
../../vm/buffer.o \
../../vm/list.o \
@@ -44,12 +46,15 @@ OBJS = \
../../jit/tree-printer.o \
../../jit/fixup-site.o \
../../jit/bc-offset-mapping.o \
+ ../../jit/exception.o \
+ ../../jit/cu-mapping.o \
../../arch/x86/emit-code$(ARCH_POSTFIX).o \
../../arch/x86/instruction.o \
../../arch/x86/insn-selector$(ARCH_POSTFIX).o \
../../arch/x86/stack-frame.o \
../../arch/x86/use-def.o \
../../arch/x86/exception$(ARCH_POSTFIX).o \
+ ../../arch/x86/unwind$(ARCH_POSTFIX).o \
$(TESTS)
TESTS = \
diff --git a/test/include/arch/exception.h b/test/include/arch/exception.h
new file mode 100644
index 0000000..dc2a743
--- /dev/null
+++ b/test/include/arch/exception.h
@@ -0,0 +1,10 @@
+#ifndef __ARCH_EXCEPTION_H
+#define __ARCH_EXCEPTION_H
+
+struct object;
+struct compilation_unit;
+
+unsigned char *throw_exception(struct compilation_unit *cu,
+ struct object *exception);
+
+#endif /* __ARCH_EXCEPTION_H */
diff --git a/test/include/arch/stack-frame.h b/test/include/arch/stack-frame.h
index 946ac33..1cdbc2d 100644
--- a/test/include/arch/stack-frame.h
+++ b/test/include/arch/stack-frame.h
@@ -1,4 +1,12 @@
#ifndef MMIX_STACK_FRAME_H
#define MMIX_STACK_FRAME_H
+#include <stdbool.h>
+
+struct jit_stack_frame {
+ unsigned long return_address;
+};
+
+bool is_jit_method(unsigned long eip);
+
#endif /* MMIX_STACK_FRAME_H */
diff --git a/test/jamvm/thread-stub.c b/test/jamvm/thread-stub.c
new file mode 100644
index 0000000..8eeb0aa
--- /dev/null
+++ b/test/jamvm/thread-stub.c
@@ -0,0 +1,8 @@
+#include <vm/vm.h>
+
+ExecEnv *getExecEnv()
+{
+ static ExecEnv ee;
+
+ return ⅇ
+}
diff --git a/test/jit/Makefile b/test/jit/Makefile
index 3ec6fd4..efd9842 100644
--- a/test/jit/Makefile
+++ b/test/jit/Makefile
@@ -39,8 +39,11 @@ OBJS = \
../libharness/libharness.o \
../jamvm/alloc-stub.o \
../jamvm/resolve-stub.o \
+ ../jamvm/thread-stub.o \
+ ../jamvm/cast-stub.o \
../arch-mmix/instruction.o \
../arch-mmix/use-def.o \
+ ../arch-mmix/stack-frame.o \
arithmetic-bc-test.o \
basic-block-test.o \
bc-test-utils.o \
--
1.6.0.6
------------------------------------------------------------------------------
Crystal Reports - New Free Runtime and 30 Day Trial
Check out the new simplified licensing option that enables
unlimited royalty-free distribution of the report engine
for externally facing server and web deployment.
http://p.sf.net/sfu/businessobjects
_______________________________________________
Jatovm-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/jatovm-devel