diff --git a/.gitignore b/.gitignore index 1ea0a323d92..5706c86ae3f 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ lcov*.info /*-fake.rb /*.dll /*.exe +/coroutine-test /*.ilk /*.res /*.pc diff --git a/common.mk b/common.mk index e6559c6ca41..c6c76717750 100644 --- a/common.mk +++ b/common.mk @@ -94,6 +94,13 @@ MAKE_ENC = -f $(ENC_MK) V="$(V)" UNICODE_HDR_DIR="$(UNICODE_HDR_DIR)" \ PRISM_BUILD_DIR = prism +COROUTINE_TEST = coroutine-test$(EXEEXT) +COROUTINE_TEST_OBJS = coroutine-main.$(OBJEXT) \ + coroutine-stack.$(OBJEXT) \ + coroutine-test_initialize_destroy.$(OBJEXT) \ + coroutine-test_transfer_repeat.$(OBJEXT) \ + coroutine-test_transfer_return.$(OBJEXT) + LIBPRISM_OBJS = \ prism/arena.$(OBJEXT) \ prism/buffer.$(OBJEXT) \ @@ -691,6 +698,7 @@ noarch_config_h = tmp/include/noarch/ruby/config.h clean: clean-ext clean-enc clean-golf clean-docs clean-extout clean-modular-gc clean-local clean-platform clean-spec clean-local:: clean-runnable $(Q)$(RM) $(ALLOBJS) $(LIBRUBY_A) $(LIBRUBY_SO) $(LIBRUBY) $(LIBRUBY_ALIASES) + $(Q)$(RM) $(COROUTINE_TEST) $(COROUTINE_TEST_OBJS) $(Q)$(RM) $(PROGRAM) $(WPROGRAM) miniruby$(EXEEXT) dmyext.$(OBJEXT) dmyenc.$(OBJEXT) $(ARCHFILE) .*.time $(Q)$(RM) y.tab.c y.output encdb.h transdb.h config.log rbconfig.rb $(ruby_pc) $(COROUTINE_H:/Context.h=/.time) $(Q)$(RM) probes.h probes.$(OBJEXT) probes.stamp ruby-glommed.$(OBJEXT) ruby.imp ChangeLog $(STATIC_RUBY)$(EXEEXT) @@ -931,8 +939,15 @@ yes-test-tool: prog PHONY $(ACTIONS_ENDGROUP) no-test-tool: PHONY +test-coroutine: $(TEST_RUNNABLE)-test-coroutine +yes-test-coroutine: $(COROUTINE_TEST) PHONY + $(ACTIONS_GROUP) + $(Q)$(exec) $(COROUTINE_TEST_RUN) + $(ACTIONS_ENDGROUP) +no-test-coroutine: PHONY + test-sample: test-basic # backward compatibility for mswin-build -test-short: btest-ruby $(DOT_WAIT) test-knownbug $(DOT_WAIT) test-basic +test-short: test-coroutine $(DOT_WAIT) btest-ruby $(DOT_WAIT) test-knownbug $(DOT_WAIT) test-basic test: test-short # Separate to skip updating encs and exts by `make -o test-precheck` @@ -1043,7 +1058,7 @@ $(ENC_MK): $(srcdir)/enc/make_encmake.rb $(srcdir)/enc/Makefile.in $(srcdir)/enc .PHONY: distclean-srcs distclean-srcs-local distclean-srcs-ext .PHONY: realclean realclean-ext realclean-local realclean-enc realclean-golf realclean-extout .PHONY: realclean-srcs realclean-srcs-local realclean-srcs-ext -.PHONY: exam check test test-short test-all btest btest-ruby test-basic test-knownbug +.PHONY: exam check test test-short test-all test-coroutine btest btest-ruby test-basic test-knownbug .PHONY: run runruby parse benchmark gdb gdb-ruby .PHONY: update-mspec update-rubyspec test-rubyspec test-spec .PHONY: touch-unicode-files @@ -1137,6 +1152,30 @@ $(COROUTINE_H:/Context.h=/.time): $(Q) $(MAKEDIRS) $(@D) @$(NULLCMD) > $@ +$(COROUTINE_TEST): $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) + $(ECHO) linking $@ + $(Q) $(COROUTINE_TEST_LINK) + +coroutine-main.$(OBJEXT): {$(VPATH)}coroutine/test/main.c + $(ECHO) compiling $(srcdir)/coroutine/test/main.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/main.c + +coroutine-stack.$(OBJEXT): {$(VPATH)}coroutine/test/stack.c + $(ECHO) compiling $(srcdir)/coroutine/test/stack.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/stack.c + +coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initialize_destroy.c + $(ECHO) compiling $(srcdir)/coroutine/test/test_initialize_destroy.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_initialize_destroy.c + +coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c + $(ECHO) compiling $(srcdir)/coroutine/test/test_transfer_repeat.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_transfer_repeat.c + +coroutine-test_transfer_return.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_return.c + $(ECHO) compiling $(srcdir)/coroutine/test/test_transfer_return.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_transfer_return.c + ### # dependencies for generated C sources. diff --git a/coroutine/test/main.c b/coroutine/test/main.c new file mode 100644 index 00000000000..7340ab7e736 --- /dev/null +++ b/coroutine/test/main.c @@ -0,0 +1,31 @@ +#include "test_initialize_destroy.h" +#include "test_transfer_repeat.h" +#include "test_transfer_return.h" + +#include + +static int +run_tests(void) +{ + int result = EXIT_SUCCESS; + + if (test_initialize_destroy() != EXIT_SUCCESS) result = EXIT_FAILURE; + if (test_transfer_repeat() != EXIT_SUCCESS) result = EXIT_FAILURE; + if (test_transfer_return() != EXIT_SUCCESS) result = EXIT_FAILURE; + + return result; +} + +int +main(void) +{ + return run_tests(); +} + +#if defined(_WIN32) +int +wmain(void) +{ + return run_tests(); +} +#endif diff --git a/coroutine/test/stack.c b/coroutine/test/stack.c new file mode 100644 index 00000000000..0bc98799654 --- /dev/null +++ b/coroutine/test/stack.c @@ -0,0 +1,49 @@ +#include "ruby/internal/config.h" + +#include "stack.h" + +#include + +#if defined(_WIN32) +# include +#elif defined(HAVE_SYS_MMAN_H) +# include +#endif + +#if !defined(MAP_ANONYMOUS) && defined(MAP_ANON) +# define MAP_ANONYMOUS MAP_ANON +#endif + +int +coroutine_stack_allocate(struct coroutine_stack *stack, size_t size) +{ +#if defined(_WIN32) + stack->base = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +#elif defined(HAVE_MMAP) && defined(MAP_ANONYMOUS) + stack->base = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (stack->base == MAP_FAILED) stack->base = NULL; +#else + stack->base = malloc(size); +#endif + + stack->size = stack->base == NULL ? 0 : size; + return stack->base == NULL ? -1 : 0; +} + +void +coroutine_stack_free(struct coroutine_stack *stack) +{ + if (stack->base == NULL) return; + +#if defined(_WIN32) + VirtualFree(stack->base, 0, MEM_RELEASE); +#elif defined(HAVE_MMAP) && defined(MAP_ANONYMOUS) + munmap(stack->base, stack->size); +#else + free(stack->base); +#endif + + stack->base = NULL; + stack->size = 0; +} diff --git a/coroutine/test/stack.h b/coroutine/test/stack.h new file mode 100644 index 00000000000..1ec63730e75 --- /dev/null +++ b/coroutine/test/stack.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +struct coroutine_stack { + void *base; + size_t size; +}; + +int coroutine_stack_allocate(struct coroutine_stack *stack, size_t size); +void coroutine_stack_free(struct coroutine_stack *stack); diff --git a/coroutine/test/test_initialize_destroy.c b/coroutine/test/test_initialize_destroy.c new file mode 100644 index 00000000000..2762ae70c06 --- /dev/null +++ b/coroutine/test/test_initialize_destroy.c @@ -0,0 +1,41 @@ +#include "ruby/internal/config.h" + +#include COROUTINE_H + +#include "stack.h" +#include "test_initialize_destroy.h" + +#include +#include + +#define STACK_SIZE (1024 * 1024) + +static COROUTINE +never_started(struct coroutine_context *from, struct coroutine_context *self) +{ + (void)from; + (void)self; + abort(); +} + +int +test_initialize_destroy(void) +{ + struct coroutine_stack stack = {0}; + struct coroutine_context main_context; + struct coroutine_context context; + + if (coroutine_stack_allocate(&stack, STACK_SIZE) != 0) { + fprintf(stderr, "failed to allocate coroutine stack\n"); + return EXIT_FAILURE; + } + + coroutine_initialize_main(&main_context); + coroutine_initialize(&context, never_started, stack.base, stack.size); + + coroutine_destroy(&context); + coroutine_destroy(&main_context); + coroutine_stack_free(&stack); + + return EXIT_SUCCESS; +} diff --git a/coroutine/test/test_initialize_destroy.h b/coroutine/test/test_initialize_destroy.h new file mode 100644 index 00000000000..5bb4a2a5671 --- /dev/null +++ b/coroutine/test/test_initialize_destroy.h @@ -0,0 +1,3 @@ +#pragma once + +int test_initialize_destroy(void); diff --git a/coroutine/test/test_transfer_repeat.c b/coroutine/test/test_transfer_repeat.c new file mode 100644 index 00000000000..50a6f971116 --- /dev/null +++ b/coroutine/test/test_transfer_repeat.c @@ -0,0 +1,141 @@ +#include "ruby/internal/config.h" + +#include COROUTINE_H + +#include "stack.h" +#include "test_transfer_repeat.h" + +#include +#include +#include + +#define STACK_SIZE (1024 * 1024) +#define TRANSFER_COUNT 1000 + +static struct coroutine_context main_context; +static struct coroutine_context worker_context; +static unsigned int yielded_iteration; +static uint32_t yielded_state; +static int completed; + +static void +check_context(const char *message, struct coroutine_context *actual, + struct coroutine_context *expected) +{ + if (actual != expected) { + fprintf(stderr, "%s: expected %p, got %p\n", + message, (void *)expected, (void *)actual); + abort(); + } +} + +static void +context_start(struct coroutine_context *from, struct coroutine_context *self) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(self->fake_stack, + (const void **)&from->stack_base, + &from->stack_size); +#else + (void)from; + (void)self; +#endif +} + +static struct coroutine_context * +transfer(struct coroutine_context *current, struct coroutine_context *target) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_start_switch_fiber(¤t->fake_stack, + target->stack_base, target->stack_size); +#endif + +#if defined(COROUTINE_SANITIZE_THREAD) + __tsan_switch_to_fiber(target->tsan_fiber, 0); +#endif + + struct coroutine_context *from = coroutine_transfer(current, target); + +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(current->fake_stack, NULL, NULL); +#endif + + return from; +} + +static COROUTINE +worker_entry(struct coroutine_context *from, struct coroutine_context *self) +{ + context_start(from, self); + + check_context("worker context was entered by", from, &main_context); + check_context("worker context received self", self, &worker_context); + + uint32_t state = UINT32_C(0x12345678); + + for (unsigned int iteration = 1; iteration <= TRANSFER_COUNT; iteration++) { + state = state * UINT32_C(1664525) + UINT32_C(1013904223); + yielded_iteration = iteration; + yielded_state = state; + + from = transfer(self, &main_context); + check_context("worker context was resumed by", from, &main_context); + + if (yielded_iteration != iteration || yielded_state != state) { + fprintf(stderr, "worker local state was not preserved\n"); + abort(); + } + } + + completed = 1; + transfer(self, &main_context); + abort(); +} + +int +test_transfer_repeat(void) +{ + struct coroutine_stack stack = {0}; + int result = EXIT_FAILURE; + + if (coroutine_stack_allocate(&stack, STACK_SIZE) != 0) { + fprintf(stderr, "failed to allocate coroutine stack\n"); + return EXIT_FAILURE; + } + + yielded_iteration = 0; + yielded_state = 0; + completed = 0; + + coroutine_initialize_main(&main_context); + coroutine_initialize(&worker_context, worker_entry, stack.base, stack.size); + + uint32_t expected_state = UINT32_C(0x12345678); + + for (unsigned int iteration = 1; iteration <= TRANSFER_COUNT; iteration++) { + expected_state = expected_state * UINT32_C(1664525) + UINT32_C(1013904223); + + struct coroutine_context *from = transfer(&main_context, &worker_context); + + if (from != &worker_context || yielded_iteration != iteration || + yielded_state != expected_state || completed) { + fprintf(stderr, "repeated coroutine transfer failed at iteration %u\n", + iteration); + goto finish; + } + } + + if (transfer(&main_context, &worker_context) != &worker_context || !completed) { + fprintf(stderr, "worker context did not complete repeated transfers\n"); + goto finish; + } + + result = EXIT_SUCCESS; + +finish: + coroutine_destroy(&worker_context); + coroutine_destroy(&main_context); + coroutine_stack_free(&stack); + + return result; +} diff --git a/coroutine/test/test_transfer_repeat.h b/coroutine/test/test_transfer_repeat.h new file mode 100644 index 00000000000..56c00799378 --- /dev/null +++ b/coroutine/test/test_transfer_repeat.h @@ -0,0 +1,3 @@ +#pragma once + +int test_transfer_repeat(void); diff --git a/coroutine/test/test_transfer_return.c b/coroutine/test/test_transfer_return.c new file mode 100644 index 00000000000..657e00387de --- /dev/null +++ b/coroutine/test/test_transfer_return.c @@ -0,0 +1,142 @@ +#include "ruby/internal/config.h" + +#include COROUTINE_H + +#include "stack.h" +#include "test_transfer_return.h" + +#include +#include + +#define STACK_SIZE (1024 * 1024) + +static struct coroutine_context main_context; +static struct coroutine_context first_context; +static struct coroutine_context second_context; + +static void +check_context(const char *message, struct coroutine_context *actual, + struct coroutine_context *expected) +{ + if (actual != expected) { + fprintf(stderr, "%s: expected %p, got %p\n", + message, (void *)expected, (void *)actual); + abort(); + } +} + +static void +context_start(struct coroutine_context *from, struct coroutine_context *self) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(self->fake_stack, + (const void **)&from->stack_base, + &from->stack_size); +#else + (void)from; + (void)self; +#endif +} + +static struct coroutine_context * +transfer(struct coroutine_context *current, struct coroutine_context *target) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_start_switch_fiber(¤t->fake_stack, + target->stack_base, target->stack_size); +#endif + +#if defined(COROUTINE_SANITIZE_THREAD) + __tsan_switch_to_fiber(target->tsan_fiber, 0); +#endif + + struct coroutine_context *from = coroutine_transfer(current, target); + +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(current->fake_stack, NULL, NULL); +#endif + + return from; +} + +static COROUTINE +second_entry(struct coroutine_context *from, struct coroutine_context *self) +{ + context_start(from, self); + + check_context("second context was entered by", from, &first_context); + check_context("second context received self", self, &second_context); + + from = transfer(self, &main_context); + check_context("second context was resumed by", from, &main_context); + + transfer(self, &first_context); + abort(); +} + +static COROUTINE +first_entry(struct coroutine_context *from, struct coroutine_context *self) +{ + context_start(from, self); + + check_context("first context was entered by", from, &main_context); + check_context("first context received self", self, &first_context); + + from = transfer(self, &second_context); + check_context("first context was resumed by", from, &second_context); + + transfer(self, &main_context); + abort(); +} + +int +test_transfer_return(void) +{ + struct coroutine_stack first_stack = {0}; + struct coroutine_stack second_stack = {0}; + int result = EXIT_FAILURE; + + if (coroutine_stack_allocate(&first_stack, STACK_SIZE) != 0 || + coroutine_stack_allocate(&second_stack, STACK_SIZE) != 0) { + fprintf(stderr, "failed to allocate coroutine stacks\n"); + goto finish; + } + + coroutine_initialize_main(&main_context); + coroutine_initialize(&first_context, first_entry, + first_stack.base, first_stack.size); + coroutine_initialize(&second_context, second_entry, + second_stack.base, second_stack.size); + + result = EXIT_SUCCESS; + + /* The original target is first_context, but second_context resumes us. */ + struct coroutine_context *from = transfer(&main_context, &first_context); + + if (from != &second_context) { + fprintf(stderr, + "coroutine_transfer returned %p, expected actual resumer %p\n", + (void *)from, (void *)&second_context); + result = EXIT_FAILURE; + } + + /* Resume second_context, which resumes first_context, which resumes us. */ + from = transfer(&main_context, &second_context); + + if (from != &first_context) { + fprintf(stderr, + "coroutine_transfer returned %p, expected actual resumer %p\n", + (void *)from, (void *)&first_context); + result = EXIT_FAILURE; + } + + coroutine_destroy(&first_context); + coroutine_destroy(&second_context); + coroutine_destroy(&main_context); + +finish: + coroutine_stack_free(&first_stack); + coroutine_stack_free(&second_stack); + + return result; +} diff --git a/coroutine/test/test_transfer_return.h b/coroutine/test/test_transfer_return.h new file mode 100644 index 00000000000..228d611cde3 --- /dev/null +++ b/coroutine/test/test_transfer_return.h @@ -0,0 +1,3 @@ +#pragma once + +int test_transfer_return(void); diff --git a/depend b/depend index a18eebe3fa9..4099bd57bac 100644 --- a/depend +++ b/depend @@ -55,6 +55,11 @@ compile.$(OBJEXT): {$(VPATH)}compile.c complex.$(OBJEXT): {$(VPATH)}complex.c concurrent_set.$(OBJEXT): {$(VPATH)}concurrent_set.c cont.$(OBJEXT): {$(VPATH)}cont.c +coroutine-main.$(OBJEXT): {$(VPATH)}coroutine/test/main.c +coroutine-stack.$(OBJEXT): {$(VPATH)}coroutine/test/stack.c +coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initialize_destroy.c +coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c +coroutine-test_transfer_return.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_return.c debug.$(OBJEXT): {$(VPATH)}debug.c debug_counter.$(OBJEXT): {$(VPATH)}debug_counter.c dir.$(OBJEXT): {$(VPATH)}dir.c diff --git a/template/Makefile.in b/template/Makefile.in index a519dfb858c..68e53e379b4 100644 --- a/template/Makefile.in +++ b/template/Makefile.in @@ -185,6 +185,8 @@ BOOTSTRAPRUBY_FAKE = $(yes_baseruby:yes=$(arch)-fake.rb) COROUTINE_H = @X_COROUTINE_H@ COROUTINE_OBJ = $(COROUTINE_H:.h=.$(OBJEXT)) COROUTINE_SRC = @X_COROUTINE_SRC@ +COROUTINE_TEST_LINK = $(CC) $(EXE_LDFLAGS) $(XLDFLAGS) $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) $(MAINLIBS) $(OUTFLAG)$(COROUTINE_TEST) +COROUTINE_TEST_RUN = ./$(COROUTINE_TEST) #### End of system configuration section. #### diff --git a/thread_sched.h b/thread_sched.h index e56f5d7dd19..2b38f08a71a 100644 --- a/thread_sched.h +++ b/thread_sched.h @@ -161,8 +161,8 @@ struct rb_native_thread { bool retiring; // A terminating coroutine records its context here before its final - // transfer; this nt's loop reclaims it. (Not via coroutine_transfer()'s - // return value: its meaning differs between the amd64 asm and ucontext.) + // transfer; this nt's loop reclaims it. coroutine_transfer() cannot be + // used because a terminating coroutine never resumes to return a value. struct coroutine_context *dead_co; }; diff --git a/win32/Makefile.sub b/win32/Makefile.sub index ccc3e918f50..897964ab028 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -344,6 +344,8 @@ COROUTINE_SRC = $(COROUTINE_OBJ:.obj=.asm) !error copy coroutine has been replaced with pthread implementation at 42130a64f02294dc8025af3a51bda518c67ab33d !endif COROUTINE_H = $(COROUTINE_OBJ:.obj=.h) +COROUTINE_TEST_LINK = $(CC) $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) $(MAINLIBS) $(OUTFLAG)$(COROUTINE_TEST) -link $(LDFLAGS) $(XLDFLAGS) +COROUTINE_TEST_RUN = .\$(COROUTINE_TEST) ARFLAGS = -machine:$(MACHINE) -out: LD = $(CC)