From ff9d635f89a763854278789e2c79c0772eea4927 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Sun, 22 Mar 2020 12:52:45 +0100 Subject: [PATCH 01/16] Stdin using notebook raw_input (#1) * Added stdin input via the notebook frontend * Added support for single line code snippets. * Updated README. * Updated Dockerfile. * Changed file cleanup to happen right after execution, rather than before kernel shutdown. --- Dockerfile | 6 +- README.md | 22 ++++--- jupyter_c_kernel/install_c_kernel | 2 +- jupyter_c_kernel/kernel.py | 87 +++++++++++++++++++++---- jupyter_c_kernel/resources/stdio_wrap.h | 10 +++ 5 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 jupyter_c_kernel/resources/stdio_wrap.h diff --git a/Dockerfile b/Dockerfile index 8ff4b8b..38acb16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM jupyter/minimal-notebook -MAINTAINER Brendan Rius +MAINTAINER Xaver Klemenschits USER root @@ -7,8 +7,8 @@ WORKDIR /tmp COPY ./ jupyter_c_kernel/ -RUN pip install --no-cache-dir jupyter_c_kernel/ -RUN cd jupyter_c_kernel && install_c_kernel --user +RUN pip install --no-cache-dir -e jupyter_c_kernel/ > piplog.txt +RUN cd jupyter_c_kernel && install_c_kernel --user > installlog.txt WORKDIR /home/$NB_USER/ diff --git a/README.md b/README.md index 7d96ad1..4c9e57a 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ ## Use with Docker (recommended) - * `docker pull brendanrius/jupyter-c-kernel` - * `docker run -p 8888:8888 brendanrius/jupyter-c-kernel` + * `docker pull xaverklemenschits/jupyter-c-kernel` + * `docker run -p 8888:8888 xaverklemenschits/jupyter-c-kernel` * Copy the given URL containing the token, and browse to it. For instance: - + ``` Copy/paste this URL into your browser when you connect for the first time, to login with a token: @@ -24,9 +24,14 @@ Works only on Linux and OS X. Windows is not supported yet. If you want to use t * pip ### Step-by-step: - * `pip install jupyter-c-kernel` - * `install_c_kernel` - * `jupyter-notebook`. Enjoy! +```bash +git clone https://github.com/XaverKlemenschits/jupyter-c-kernel.git +cd jupyter-c-kernel +pip install -e . +cd jupyter_c_kernel && install_c_kernel --user +# now you can start the notebook +jupyter notebook +``` ## Example of notebook @@ -47,9 +52,10 @@ change the code in real-time in Docker. For that, just run the docker box like that: ```bash -git clone https://github.com/brendan-rius/jupyter-c-kernel.git +git clone https://github.com/XaverKlemenschits/jupyter-c-kernel.git cd jupyter-c-kernel -docker run -v $(pwd):/jupyter/jupyter_c_kernel/ -p 8888:8888 brendanrius/jupyter-c-kernel +docker build -t myName/jupyter . +docker run -v $(pwd):/tmp/jupyter_c_kernel/ -p 8888:8888 myName/jupyter ``` This clones the source, run the kernel, and binds the current folder (the one diff --git a/jupyter_c_kernel/install_c_kernel b/jupyter_c_kernel/install_c_kernel index fcd009d..4b23026 100644 --- a/jupyter_c_kernel/install_c_kernel +++ b/jupyter_c_kernel/install_c_kernel @@ -29,7 +29,7 @@ def install_my_kernel_spec(user=True, prefix=None): # TODO: Copy resources once they're specified print('Installing IPython kernel spec') - KernelSpecManager().install_kernel_spec(td, 'c', user=user, replace=True, prefix=prefix) + KernelSpecManager().install_kernel_spec(td, 'c', user=user, prefix=prefix) def _is_root(): diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 7ed7e71..81f97a2 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -14,7 +14,9 @@ class RealTimeSubprocess(subprocess.Popen): A subprocess that allows to read its stdout and stderr in real time """ - def __init__(self, cmd, write_to_stdout, write_to_stderr): + inputRequest = "" + + def __init__(self, cmd, write_to_stdout, write_to_stderr, read_from_stdin): """ :param cmd: the command to execute :param write_to_stdout: a callable that will be called with chunks of data from stdout @@ -22,8 +24,9 @@ def __init__(self, cmd, write_to_stdout, write_to_stderr): """ self._write_to_stdout = write_to_stdout self._write_to_stderr = write_to_stderr + self._read_from_stdin = read_from_stdin - super().__init__(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) + super().__init__(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=0) self._stdout_queue = Queue() self._stdout_thread = Thread(target=RealTimeSubprocess._enqueue_output, args=(self.stdout, self._stdout_queue)) @@ -60,10 +63,23 @@ def read_all_from_queue(queue): stdout_contents = read_all_from_queue(self._stdout_queue) if stdout_contents: - self._write_to_stdout(stdout_contents) + contents = stdout_contents.decode() + # if there is input request, make output and then + # ask frontend for input + start = contents.find(self.__class__.inputRequest) + if(start >= 0): + contents = contents.replace(self.__class__.inputRequest, '') + if(len(contents) > 0): + self._write_to_stdout(contents) + readLine = self._read_from_stdin() + self.stdin.write(readLine.encode()) + self.stdin.write(b"\n") + else: + self._write_to_stdout(contents) + stderr_contents = read_all_from_queue(self._stderr_queue) if stderr_contents: - self._write_to_stderr(stderr_contents) + self._write_to_stderr(stderr_contents.decode()) class CKernel(Kernel): @@ -71,25 +87,36 @@ class CKernel(Kernel): implementation_version = '1.0' language = 'c' language_version = 'C11' - language_info = {'name': 'c', + language_info = {'name': 'text/x-csrc', 'mimetype': 'text/plain', 'file_extension': '.c'} banner = "C kernel.\n" \ "Uses gcc, compiles in C11, and creates source code files and executables in temporary folder.\n" + main_head = "#include \n" \ + "#include \n" \ + "int main(){\n" + + main_foot = "\nreturn 0;\n}" + def __init__(self, *args, **kwargs): super(CKernel, self).__init__(*args, **kwargs) + self._allow_stdin = True self.files = [] mastertemp = tempfile.mkstemp(suffix='.out') os.close(mastertemp[0]) self.master_path = mastertemp[1] - filepath = path.join(path.dirname(path.realpath(__file__)), 'resources', 'master.c') + self.resDir = path.join(path.dirname(path.realpath(__file__)), 'resources') + filepath = path.join(self.resDir, 'master.c') subprocess.call(['gcc', filepath, '-std=c11', '-rdynamic', '-ldl', '-o', self.master_path]) def cleanup_files(self): """Remove all the temporary files created by the kernel""" + # keep the list of files create in case there is an exception + # before they can be deleted as usual for file in self.files: - os.remove(file) + if(os.path.exists(file)): + os.remove(file) os.remove(self.master_path) def new_temp_file(self, **kwargs): @@ -107,10 +134,14 @@ def _write_to_stdout(self, contents): def _write_to_stderr(self, contents): self.send_response(self.iopub_socket, 'stream', {'name': 'stderr', 'text': contents}) + def _read_from_stdin(self): + return self.raw_input() + def create_jupyter_subprocess(self, cmd): return RealTimeSubprocess(cmd, - lambda contents: self._write_to_stdout(contents.decode()), - lambda contents: self._write_to_stderr(contents.decode())) + self._write_to_stdout, + self._write_to_stderr, + self._read_from_stdin) def compile_with_gcc(self, source_filename, binary_filename, cflags=None, ldflags=None): cflags = ['-std=c11', '-fPIC', '-shared', '-rdynamic'] + cflags @@ -123,6 +154,8 @@ def _filter_magics(self, code): 'ldflags': [], 'args': []} + actualCode = '' + for line in code.splitlines(): if line.startswith('//%'): key, value = line[3:].split(":", 2) @@ -136,12 +169,33 @@ def _filter_magics(self, code): for argument in re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', value): magics['args'] += [argument.strip('"')] - return magics + # only keep lines which did not contain magics + else: + actualCode += line + '\n' + + return magics, actualCode + + # check whether int main() is specified, if not add it around the code + # also add common magics like -lm + def _add_main(self, magics, code): + x = re.search("int\s+main\s*\(", code) + if x is None: + code = self.main_head + code + self.main_foot + magics['cflags'] += ['-lm'] + + return magics, code def do_execute(self, code, silent, store_history=True, - user_expressions=None, allow_stdin=False): + user_expressions=None, allow_stdin=True): + + magics, code = self._filter_magics(code) + + magics, code = self._add_main(magics, code) - magics = self._filter_magics(code) + # replace stdio with wrapped version + headerDir = "\"" + self.resDir + "/stdio_wrap.h" + "\"" + code = code.replace("", headerDir) + code = code.replace("\"stdio.h\"", headerDir) with self.new_temp_file(suffix='.c') as source_file: source_file.write(code) @@ -155,6 +209,11 @@ def do_execute(self, code, silent, store_history=True, self._write_to_stderr( "[C kernel] GCC exited with code {}, the executable will not be executed".format( p.returncode)) + + # delete source files before exit + os.remove(source_file.name) + os.remove(binary_file.name) + return {'status': 'ok', 'execution_count': self.execution_count, 'payload': [], 'user_expressions': {}} @@ -163,6 +222,10 @@ def do_execute(self, code, silent, store_history=True, p.write_contents() p.write_contents() + # now remove the files we have just created + os.remove(source_file.name) + os.remove(binary_file.name) + if p.returncode != 0: self._write_to_stderr("[C kernel] Executable exited with code {}".format(p.returncode)) return {'status': 'ok', 'execution_count': self.execution_count, 'payload': [], 'user_expressions': {}} diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h new file mode 100644 index 0000000..0374711 --- /dev/null +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -0,0 +1,10 @@ +#include + +/* Replace all the necessary input functions */ +#define scanf(...) printf("");\ +fflush(stdout);\ +scanf(__VA_ARGS__); + +#define getchar() printf("");\ +fflush(stdout);\ +getchar(); From 8b69d8354d1640bcb80f1054dde7a46d7060cba6 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Fri, 27 Mar 2020 11:26:35 +0100 Subject: [PATCH 02/16] Added stdio_wrap.h to MANIFEST to have it copied during install. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index ce43725..16c2e89 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include jupyter_c_kernel/resources/master.c +include jupyter_c_kernel/resources/stdio_wrap.h include README.md LICENSE.txt From bddb7ca6b0c4ffed44132e971b953b929b263583 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Fri, 27 Mar 2020 12:27:14 +0100 Subject: [PATCH 03/16] Static server (#2) * Wrap input and file output functions for a read only static server. * Added -y to docker apt-get in docker file. * wError/wAll as options in Kernel, defaults only to wAll. --- Dockerfile | 4 ++ jupyter_c_kernel/kernel.py | 18 ++++++- jupyter_c_kernel/resources/stdio_wrap.h | 65 ++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 38acb16..3480d3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,10 @@ MAINTAINER Xaver Klemenschits USER root +# Install vim and ssh +RUN apt-get update +RUN apt-get install -y vim openssh-client + WORKDIR /tmp COPY ./ jupyter_c_kernel/ diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 81f97a2..502ac9c 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -102,6 +102,9 @@ class CKernel(Kernel): def __init__(self, *args, **kwargs): super(CKernel, self).__init__(*args, **kwargs) self._allow_stdin = True + self.readOnlyFileSystem = False + self.wAll = True # show all warnings by default + self.wError = False # but keep comipiling for warnings self.files = [] mastertemp = tempfile.mkstemp(suffix='.out') os.close(mastertemp[0]) @@ -145,6 +148,12 @@ def create_jupyter_subprocess(self, cmd): def compile_with_gcc(self, source_filename, binary_filename, cflags=None, ldflags=None): cflags = ['-std=c11', '-fPIC', '-shared', '-rdynamic'] + cflags + if self.wError: + cflags = cflags + ['-Werror'] + if self.wAll: + cflags = cflags + ['-Wall'] + if self.readOnlyFileSystem: + cflags = ['-DREAD_ONLY_FILE_SYSTEM'] + cflags args = ['gcc', source_filename] + cflags + ['-o', binary_filename] + ldflags return self.create_jupyter_subprocess(args) @@ -178,8 +187,13 @@ def _filter_magics(self, code): # check whether int main() is specified, if not add it around the code # also add common magics like -lm def _add_main(self, magics, code): - x = re.search("int\s+main\s*\(", code) - if x is None: + # remove comments + tmpCode = re.sub("//.*", "", code) + tmpCode = re.sub("/\*.*?\*/", "", tmpCode, flags=re.M|re.S) + + x = re.search("int\s+main\s*\(", tmpCode) + + if not x: code = self.main_head + code + self.main_foot magics['cflags'] += ['-lm'] diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index 0374711..e7a1ebf 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -1,10 +1,63 @@ +#ifndef STDIO_WRAP_H +#define STDIO_WRAP_H + #include +#include + +/* Define the functions to overload the old ones */ +int scanf_wrap(const char *format, ...) { + printf(""); + fflush(stdout); + va_list arglist; + va_start( arglist, format ); + int result = vscanf( format, arglist ); + va_end( arglist ); + return result; +} + +int getchar_wrap(){ + printf(""); + fflush(stdout); + return getchar(); +} + +/* Replace all the necessary input functions + Need double hashes in case there are no __VA_ARGS__*/ +#define scanf(format, ...) scanf_wrap(format, ##__VA_ARGS__) + +#define getchar() getchar_wrap() + +/* Replace FILE write operations for read-only systems */ +#ifdef READ_ONLY_FILE_SYSTEM + +/* Define wrapping functions */ +/* Output that the fopen succeeded and return some valid pointer */ +FILE *fopen_wrap(const char *filename, const char *modes) { + static long stream = 0x1FFFF0000; +#ifdef SHOW_FILE_IO_VERBOSE + printf("\x01b[42m"); + printf("\"%s\" opened in mode \"%s\"\n", filename, modes); + printf("\x01b[0m"); +#endif /* SHOW_FILE_IO_VERBOSE */ + return (FILE*)stream++; +} + +int fprintf_wrap(FILE* stream, const char* format, ...) { + printf("\x01b[42m"); + printf("%p:", stream); + printf("\x01b[0m"); + va_list arglist; + va_start( arglist, format ); + int result = vprintf(format, arglist); + va_end( arglist ); + return result; +} /* Replace all the necessary input functions */ -#define scanf(...) printf("");\ -fflush(stdout);\ -scanf(__VA_ARGS__); +#define fopen(file, mode) fopen_wrap(file, mode) + +#define fprintf(stream, format, ...) fprintf_wrap(stream, format, ##__VA_ARGS__) + +#endif /* READ_ONLY_FILE_SYSTEM */ -#define getchar() printf("");\ -fflush(stdout);\ -getchar(); +#endif /* STDIO_WRAP_H */ From 3429b362baec76769ffe7b578599b7de6608efe6 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Fri, 3 Apr 2020 13:48:32 +0200 Subject: [PATCH 04/16] Added input buffer in stdio_wrap.h to mimic command line behaviour. Added wait on output to avoid race condition with program termination. --- jupyter_c_kernel/kernel.py | 17 +++++-- jupyter_c_kernel/resources/stdio_wrap.h | 64 +++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 502ac9c..68eda6a 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -72,8 +72,10 @@ def read_all_from_queue(queue): if(len(contents) > 0): self._write_to_stdout(contents) readLine = self._read_from_stdin() + # need to add newline since it is not captured by frontend + readLine += "\n" + os.write(1, readLine.encode()); self.stdin.write(readLine.encode()) - self.stdin.write(b"\n") else: self._write_to_stdout(contents) @@ -88,7 +90,7 @@ class CKernel(Kernel): language = 'c' language_version = 'C11' language_info = {'name': 'text/x-csrc', - 'mimetype': 'text/plain', + 'mimetype': 'text/x-csrc', 'file_extension': '.c'} banner = "C kernel.\n" \ "Uses gcc, compiles in C11, and creates source code files and executables in temporary folder.\n" @@ -188,10 +190,10 @@ def _filter_magics(self, code): # also add common magics like -lm def _add_main(self, magics, code): # remove comments - tmpCode = re.sub("//.*", "", code) - tmpCode = re.sub("/\*.*?\*/", "", tmpCode, flags=re.M|re.S) + tmpCode = re.sub(r"//.*", "", code) + tmpCode = re.sub(r"/\*.*?\*/", "", tmpCode, flags=re.M|re.S) - x = re.search("int\s+main\s*\(", tmpCode) + x = re.search(r"int\s+main\s*\(", tmpCode) if not x: code = self.main_head + code + self.main_foot @@ -234,6 +236,11 @@ def do_execute(self, code, silent, store_history=True, p = self.create_jupyter_subprocess([self.master_path, binary_file.name] + magics['args']) while p.poll() is None: p.write_contents() + + # wait for threads to finish, so output is always shown + p._stdout_thread.join() + p._stderr_thread.join() + p.write_contents() # now remove the files we have just created diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index e7a1ebf..9c431cc 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -3,22 +3,76 @@ #include #include +#include + +/* Need input buffer to know whether we need another input request */ +char inputBuff[1<<10] = ""; + +/* read remaining input into buffer so it can be used in next call */ +void readIntoBuffer() { + long length = strlen(inputBuff); + char nextChar = 0; + while((nextChar = getchar()) != '\n'){ + inputBuff[length++] = nextChar; + } + inputBuff[length++] = '\n'; + inputBuff[length] = '\0'; +} /* Define the functions to overload the old ones */ int scanf_wrap(const char *format, ...) { - printf(""); - fflush(stdout); + /* unget chars in buffer */ + int doRequest = 1; + long index = strlen(inputBuff) - 1; + // printf("Start index: %ld\n", index); + for(; index >= 0; --index) { + ungetc(inputBuff[index], stdin); + /* if there already is a newline in buffer + we need no input request */ + if(inputBuff[index] == '\n') { + doRequest = 0; + } + } + /* Buffer will always be empty after scanf call + since there is no way to enter more than one + newline in the frontend */ + inputBuff[0] = '\0'; + // printf("doRequest: %d\n", doRequest); + if(doRequest) { + printf(""); + fflush(stdout); + } va_list arglist; va_start( arglist, format ); int result = vscanf( format, arglist ); va_end( arglist ); + return result; } int getchar_wrap(){ - printf(""); - fflush(stdout); - return getchar(); + /* check if there is still something in the input buffer*/ + char input = 0; + long length = strlen(inputBuff); + if(length <= 0) { + printf(""); + fflush(stdout); + + input = getchar(); + readIntoBuffer(); + } else { + input = inputBuff[0]; + /* shift all chars one to the left */ + int i = 1; + for(; i < 100; ++i){ + inputBuff[i-1] = inputBuff[i]; + if(inputBuff[i] == '\0') { + break; + } + } + } + + return input; } /* Replace all the necessary input functions From ecbea115c78e9a8fae0b3aea360de81412bced9a Mon Sep 17 00:00:00 2001 From: Xaver K Date: Fri, 3 Apr 2020 14:07:04 +0200 Subject: [PATCH 05/16] Cleanup. --- jupyter_c_kernel/kernel.py | 1 - jupyter_c_kernel/resources/stdio_wrap.h | 1 - 2 files changed, 2 deletions(-) diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 68eda6a..99243fc 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -74,7 +74,6 @@ def read_all_from_queue(queue): readLine = self._read_from_stdin() # need to add newline since it is not captured by frontend readLine += "\n" - os.write(1, readLine.encode()); self.stdin.write(readLine.encode()) else: self._write_to_stdout(contents) diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index 9c431cc..e9bdc59 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -24,7 +24,6 @@ int scanf_wrap(const char *format, ...) { /* unget chars in buffer */ int doRequest = 1; long index = strlen(inputBuff) - 1; - // printf("Start index: %ld\n", index); for(; index >= 0; --index) { ungetc(inputBuff[index], stdin); /* if there already is a newline in buffer From f8e99ec24cacabdf392e96037fecb986db625de4 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Thu, 9 Apr 2020 13:23:56 +0200 Subject: [PATCH 06/16] Fixed combined getchar and scanf input to mimic command line. --- jupyter_c_kernel/resources/stdio_wrap.h | 28 ++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index e9bdc59..8a365d8 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -12,31 +12,32 @@ char inputBuff[1<<10] = ""; void readIntoBuffer() { long length = strlen(inputBuff); char nextChar = 0; - while((nextChar = getchar()) != '\n'){ - inputBuff[length++] = nextChar; - } - inputBuff[length++] = '\n'; - inputBuff[length] = '\0'; + while((nextChar = getchar()) != '\n' && nextChar != EOF){ + inputBuff[length++] = nextChar; + } + inputBuff[length++] = '\n'; + inputBuff[length] = '\0'; } /* Define the functions to overload the old ones */ int scanf_wrap(const char *format, ...) { /* unget chars in buffer */ - int doRequest = 1; + char doRequest = 1; + char leadingNewline = 1; long index = strlen(inputBuff) - 1; for(; index >= 0; --index) { ungetc(inputBuff[index], stdin); /* if there already is a newline in buffer we need no input request */ if(inputBuff[index] == '\n') { - doRequest = 0; + if(!leadingNewline){ + doRequest = 0; + } + } else { + leadingNewline = 0; } } - /* Buffer will always be empty after scanf call - since there is no way to enter more than one - newline in the frontend */ - inputBuff[0] = '\0'; - // printf("doRequest: %d\n", doRequest); + if(doRequest) { printf(""); fflush(stdout); @@ -46,6 +47,9 @@ int scanf_wrap(const char *format, ...) { int result = vscanf( format, arglist ); va_end( arglist ); + /* Put the remaining input into the input buffer */ + readIntoBuffer(); + return result; } From 8e97b754aaef36293efea7e777593306621948e0 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Fri, 17 Apr 2020 19:27:16 +0200 Subject: [PATCH 07/16] Support for other standards than c11 (#3) * Finished C89/C90/C95/C99 support. Version can only be switched in kernel or with magics. Might be worth having separate kernels for each standard. * Added option to link maths library. On by default. * Fixed empty input error. Now input loops until there is input with length > 0. --- example-notebook.ipynb | 21 +++----- jupyter_c_kernel/kernel.py | 12 ++++- jupyter_c_kernel/resources/stdio_wrap.h | 72 ++++++++++++++++++++----- 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/example-notebook.ipynb b/example-notebook.ipynb index dcd2844..95b78b5 100644 --- a/example-notebook.ipynb +++ b/example-notebook.ipynb @@ -10,9 +10,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stderr", @@ -32,6 +30,7 @@ "\n", "int main() {\n", " printf(\"Hello world\\n\");\n", + " return 0;\n", "}" ] }, @@ -52,9 +51,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stderr", @@ -98,9 +95,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stderr", @@ -142,14 +137,14 @@ "kernelspec": { "display_name": "C", "language": "c", - "name": "c_kernel" + "name": "c" }, "language_info": { - "file_extension": "c", + "file_extension": ".c", "mimetype": "text/plain", - "name": "c" + "name": "text/x-c++src" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 99243fc..6194234 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -71,7 +71,9 @@ def read_all_from_queue(queue): contents = contents.replace(self.__class__.inputRequest, '') if(len(contents) > 0): self._write_to_stdout(contents) - readLine = self._read_from_stdin() + readLine = "" + while(len(readLine) == 0): + readLine = self._read_from_stdin() # need to add newline since it is not captured by frontend readLine += "\n" self.stdin.write(readLine.encode()) @@ -104,6 +106,7 @@ def __init__(self, *args, **kwargs): super(CKernel, self).__init__(*args, **kwargs) self._allow_stdin = True self.readOnlyFileSystem = False + self.linkMaths = True # always link math library self.wAll = True # show all warnings by default self.wError = False # but keep comipiling for warnings self.files = [] @@ -148,7 +151,12 @@ def create_jupyter_subprocess(self, cmd): self._read_from_stdin) def compile_with_gcc(self, source_filename, binary_filename, cflags=None, ldflags=None): - cflags = ['-std=c11', '-fPIC', '-shared', '-rdynamic'] + cflags + # cflags = ['-std=c89', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags + # cflags = ['-std=iso9899:199409', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags + # cflags = ['-std=c99', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags + cflags = ['-std=c11', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags + if self.linkMaths: + cflags = cflags + ['-lm'] if self.wError: cflags = cflags + ['-Werror'] if self.wAll: diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index 8a365d8..e079947 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -5,8 +5,20 @@ #include #include +/* Figure out used C standard. + __STDC_VERSION__ is not always defined until C99. + If it is not defined, set standard to C89. + It is safest to set it by hand, to make sure */ +#ifdef __STDC_VERSION__ +#if __STDC_VERSION__ <= 199409L +#define C89_SUPPORT +#endif /* __STDC_VERSION__ <= 199409L */ +#else /* __STDC_VERSION__ */ +#define C89_SUPPORT +#endif /* __STDC_VERSION__ */ + /* Need input buffer to know whether we need another input request */ -char inputBuff[1<<10] = ""; +static char inputBuff[1<<10] = ""; /* read remaining input into buffer so it can be used in next call */ void readIntoBuffer() { @@ -19,8 +31,8 @@ void readIntoBuffer() { inputBuff[length] = '\0'; } -/* Define the functions to overload the old ones */ -int scanf_wrap(const char *format, ...) { +/* check whether input request is needed */ +char checkInputRequest() { /* unget chars in buffer */ char doRequest = 1; char leadingNewline = 1; @@ -38,21 +50,49 @@ int scanf_wrap(const char *format, ...) { } } + return doRequest; +} + +/* Define the functions to overload the old ones */ + +/* Wrapping of scanf depends on standard */ +#ifdef C89_SUPPORT +/* Need to define vscanf for c89. + TODO: This is a bit risky, since the underlying glibc does not + have to include this if it is old. If it does not, linking will fail. + The only safe way is reimplementing the whole function. */ + +/* Read formatted input from stdin into argument list ARG. + + This function is a possible cancellation point and therefore not + marked with __THROW. */ +extern int vscanf (const char *__restrict __format, _G_va_list __arg) + __attribute__ ((__format__ (__scanf__, 1, 0))) __wur; +#endif /* C89_SUPPORT */ + +int scanf_wrap(const char *format, ...) { + char doRequest = checkInputRequest(); + if(doRequest) { printf(""); fflush(stdout); } - va_list arglist; - va_start( arglist, format ); - int result = vscanf( format, arglist ); - va_end( arglist ); - /* Put the remaining input into the input buffer */ - readIntoBuffer(); + { + va_list arglist; + int result; + va_start( arglist, format ); + result = vscanf( format, arglist ); + va_end( arglist ); - return result; + /* Put the remaining input into the input buffer */ + readIntoBuffer(); + + return result; + } } + int getchar_wrap(){ /* check if there is still something in the input buffer*/ char input = 0; @@ -64,9 +104,10 @@ int getchar_wrap(){ input = getchar(); readIntoBuffer(); } else { + int i = 1; + input = inputBuff[0]; /* shift all chars one to the left */ - int i = 1; for(; i < 100; ++i){ inputBuff[i-1] = inputBuff[i]; if(inputBuff[i] == '\0') { @@ -79,8 +120,15 @@ int getchar_wrap(){ } /* Replace all the necessary input functions - Need double hashes in case there are no __VA_ARGS__*/ + depending on the language version used */ +#ifndef C89_SUPPORT +/* Need double hashes in case there are no __VA_ARGS__*/ #define scanf(format, ...) scanf_wrap(format, ##__VA_ARGS__) +#else /* C89_SUPPORT */ +/* Since there are no variadic macros in C89, this is the only way + although it is horrible */ +#define scanf scanf_wrap +#endif /* C89_SUPPORT */ #define getchar() getchar_wrap() From 7c39e852efe0d00faac7566aea6c13793147d4e2 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Fri, 8 May 2020 15:53:37 +0200 Subject: [PATCH 08/16] Fixed delayed stderr output. --- README.md | 4 ++-- jupyter_c_kernel/kernel.py | 9 +++++---- jupyter_c_kernel/resources/stdio_wrap.h | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4c9e57a..47912c6 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Works only on Linux and OS X. Windows is not supported yet. If you want to use t ```bash git clone https://github.com/XaverKlemenschits/jupyter-c-kernel.git cd jupyter-c-kernel -pip install -e . -cd jupyter_c_kernel && install_c_kernel --user +pip install -e . # for system install: sudo install . +cd jupyter_c_kernel && install_c_kernel --user # for sys install: sudo install_c_kernel # now you can start the notebook jupyter notebook ``` diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 6194234..acdc0c7 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -61,6 +61,10 @@ def read_all_from_queue(queue): size -= 1 return res + stderr_contents = read_all_from_queue(self._stderr_queue) + if stderr_contents: + self._write_to_stderr(stderr_contents.decode()) + stdout_contents = read_all_from_queue(self._stdout_queue) if stdout_contents: contents = stdout_contents.decode() @@ -80,10 +84,6 @@ def read_all_from_queue(queue): else: self._write_to_stdout(contents) - stderr_contents = read_all_from_queue(self._stderr_queue) - if stderr_contents: - self._write_to_stderr(stderr_contents.decode()) - class CKernel(Kernel): implementation = 'jupyter_c_kernel' @@ -152,6 +152,7 @@ def create_jupyter_subprocess(self, cmd): def compile_with_gcc(self, source_filename, binary_filename, cflags=None, ldflags=None): # cflags = ['-std=c89', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags + # cflags = ['-std=c99', '-Wdeclaration-after-statement', '-Wvla', '-fPIC', '-shared', '-rdynamic'] + cflags # cflags = ['-std=iso9899:199409', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags # cflags = ['-std=c99', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags cflags = ['-std=c11', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index e079947..62d5fa7 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -60,7 +60,7 @@ char checkInputRequest() { /* Need to define vscanf for c89. TODO: This is a bit risky, since the underlying glibc does not have to include this if it is old. If it does not, linking will fail. - The only safe way is reimplementing the whole function. */ + The better way would be readin via sscanf. */ /* Read formatted input from stdin into argument list ARG. From 449e682b18a2e94dad9e79b6724ae1834b0b4200 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Mon, 11 May 2020 19:03:22 +0200 Subject: [PATCH 09/16] Introduced buffered output option to replicate terminal behaviour. --- jupyter_c_kernel/kernel.py | 3 + jupyter_c_kernel/resources/stdio_wrap.h | 194 ++++++++++++++++++++---- 2 files changed, 164 insertions(+), 33 deletions(-) diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index acdc0c7..44d3095 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -106,6 +106,7 @@ def __init__(self, *args, **kwargs): super(CKernel, self).__init__(*args, **kwargs) self._allow_stdin = True self.readOnlyFileSystem = False + self.bufferedOutput = False self.linkMaths = True # always link math library self.wAll = True # show all warnings by default self.wError = False # but keep comipiling for warnings @@ -164,6 +165,8 @@ def compile_with_gcc(self, source_filename, binary_filename, cflags=None, ldflag cflags = cflags + ['-Wall'] if self.readOnlyFileSystem: cflags = ['-DREAD_ONLY_FILE_SYSTEM'] + cflags + if self.bufferedOutput: + cflags = ['-DBUFFERED_OUTPUT'] + cflags args = ['gcc', source_filename] + cflags + ['-o', binary_filename] + ldflags return self.create_jupyter_subprocess(args) diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index 62d5fa7..8234ef5 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include /* Figure out used C standard. __STDC_VERSION__ is not always defined until C99. @@ -18,6 +20,7 @@ #endif /* __STDC_VERSION__ */ /* Need input buffer to know whether we need another input request */ +/* TODO allocate this dynamically */ static char inputBuff[1<<10] = ""; /* read remaining input into buffer so it can be used in next call */ @@ -33,60 +36,78 @@ void readIntoBuffer() { /* check whether input request is needed */ char checkInputRequest() { - /* unget chars in buffer */ - char doRequest = 1; - char leadingNewline = 1; - long index = strlen(inputBuff) - 1; - for(; index >= 0; --index) { - ungetc(inputBuff[index], stdin); - /* if there already is a newline in buffer - we need no input request */ - if(inputBuff[index] == '\n') { - if(!leadingNewline){ - doRequest = 0; - } - } else { - leadingNewline = 0; - } - } - - return doRequest; + return strlen(inputBuff) <= 0; } -/* Define the functions to overload the old ones */ - +/* Define the input functions to overload the old ones */ /* Wrapping of scanf depends on standard */ #ifdef C89_SUPPORT /* Need to define vscanf for c89. TODO: This is a bit risky, since the underlying glibc does not have to include this if it is old. If it does not, linking will fail. - The better way would be readin via sscanf. */ + The robust way would be readin via sscanf. */ /* Read formatted input from stdin into argument list ARG. This function is a possible cancellation point and therefore not marked with __THROW. */ -extern int vscanf (const char *__restrict __format, _G_va_list __arg) - __attribute__ ((__format__ (__scanf__, 1, 0))) __wur; + extern int vsscanf (const char *__restrict __s, + const char *__restrict __format, _G_va_list __arg) + __THROW __attribute__ ((__format__ (__scanf__, 2, 0))); #endif /* C89_SUPPORT */ int scanf_wrap(const char *format, ...) { char doRequest = checkInputRequest(); + char *inputString = 0; if(doRequest) { printf(""); fflush(stdout); + /* read everything from stdin into buffer */ + readIntoBuffer(); + } + + /* make substring from inputBuff */ + { + const long length = strlen(inputBuff); + long index = 0; + char leadingSpace = 1; + for(; index < length; ++index) { + if(isspace(inputBuff[index])) { + if(!leadingSpace) { + break; + } + } else { + leadingSpace = 0; + } + } + + inputString = malloc(index + 1); + strncpy(inputString, inputBuff, index); + inputString[index] = '\0'; + /* now move inputBuff up */ + { + long a = 0; + leadingSpace = 1; + /* +1 to include \0 */ + for(; index < length + 1; ++index) { + if(!leadingSpace || isspace(inputBuff[index]) == 0) { + leadingSpace = 0; + inputBuff[a] = inputBuff[index]; + ++a; + } + } + } } { va_list arglist; int result; - va_start( arglist, format ); - result = vscanf( format, arglist ); - va_end( arglist ); + va_start(arglist, format); + result = vsscanf(inputString, format, arglist); + va_end(arglist); - /* Put the remaining input into the input buffer */ - readIntoBuffer(); + free(inputString); return result; } @@ -101,14 +122,15 @@ int getchar_wrap(){ printf(""); fflush(stdout); - input = getchar(); readIntoBuffer(); - } else { - int i = 1; + } - input = inputBuff[0]; + input = inputBuff[0]; + { + long i = 1; + long length = strlen(inputBuff) + 1; /* shift all chars one to the left */ - for(; i < 100; ++i){ + for(; i < length; ++i){ inputBuff[i-1] = inputBuff[i]; if(inputBuff[i] == '\0') { break; @@ -132,6 +154,112 @@ int getchar_wrap(){ #define getchar() getchar_wrap() +/* output functions to replicate terminal behaviour */ +#ifdef BUFFERED_OUTPUT +/* buffer for all output */ +/* TODO allocate this dynamically */ +static char outputBuff[1<<10] = ""; +static char attachedOutputFlush = 0; + +void flush_all_output() { + printf("%s", outputBuff); + fflush(stdout); + outputBuff[0] = '\0'; +} + +/* Flush all output on exit */ +void attachOutputFlush() { + if(attachedOutputFlush == 0){ + int error = atexit(flush_all_output); + if(error != 0) { + fprintf(stderr, "ERROR: Could not set exit function! Error %d\n", error); + } + attachedOutputFlush = 1; + } +} + +/* this function is called to check whether there + is a '\n' in the output that should be flushed */ +void flush_until_newline() { + long i = 0; + long length = strlen(outputBuff); + for(; i < length; ++i) { + if(outputBuff[i] == '\n') { + char *printBuff = malloc(i+2); + strncpy(printBuff, outputBuff, i+1); + printBuff[i+1] = '\0'; + printf("%s", printBuff); + free(printBuff); + /* now remove the printed string from the buffer + and start again */ + { + long a = 0; + ++i; + /* +1 to include \0 */ + for(; i < length + 1; ++a, ++i) { + outputBuff[a] = outputBuff[i]; + } + i = 0; + length = strlen(outputBuff); + } + } + } +} + +/* for printf, print all to a string. + Then cycle through all chars and see if \n is + written. If there is one, flush the output, otherwise + write to buffer */ +int printf_wrap(const char *format, ...) { + /* append output to buffer */ + va_list arglist; + int result; + va_start( arglist, format ); + result = vsprintf(outputBuff + strlen(outputBuff), format, arglist); + va_end( arglist ); + + /* Now flush if there is a reason to */ + flush_until_newline(); + + attachOutputFlush(); + + return result; +} + +int putchar_wrap(int c) { + long length = strlen(outputBuff); + outputBuff[length] = (char)c; + outputBuff[length+1] = '\0'; + if(c == '\n') { + flush_until_newline(); + } + + attachOutputFlush(); + + return c; +} + +int fflush_wrap(FILE* stream) { + if(stream == stdout) { + flush_all_output(); + } + return fflush(stream); +} + +int fclose_wrap(FILE* stream) { + if(stream == stdout) { + flush_all_output(); + } + return fclose(stream); +} + +#define printf printf_wrap +#define putchar putchar_wrap +#define fflush fflush_wrap +#define fclose fclose_wrap + +#endif /* BUFFERED_OUTPUT */ + /* Replace FILE write operations for read-only systems */ #ifdef READ_ONLY_FILE_SYSTEM From dddbfe9e85cc785deb89a7f9f3e8b9cec7513b20 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Mon, 11 May 2020 19:53:02 +0200 Subject: [PATCH 10/16] Ditched C89 support for number of chars read by scanf. --- jupyter_c_kernel/resources/stdio_wrap.h | 267 ++++++++++++------------ 1 file changed, 133 insertions(+), 134 deletions(-) diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index 8234ef5..258f37b 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -19,9 +19,110 @@ #define C89_SUPPORT #endif /* __STDC_VERSION__ */ +/* output functions to replicate terminal behaviour */ +#ifdef BUFFERED_OUTPUT +/* buffer for all output */ +/* TODO allocate this dynamically */ +static char outputBuff[1<<10] = ""; +static char attachedOutputFlush = 0; + +void flush_all_output() { + printf("%s", outputBuff); + fflush(stdout); + outputBuff[0] = '\0'; +} + +/* Flush all output on exit */ +void attachOutputFlush() { + if(attachedOutputFlush == 0){ + int error = atexit(flush_all_output); + if(error != 0) { + fprintf(stderr, "ERROR: Could not set exit function! Error %d\n", error); + } + attachedOutputFlush = 1; + } +} + +/* this function is called to check whether there + is a '\n' in the output that should be flushed */ +void flush_until_newline() { + long i = 0; + long length = strlen(outputBuff); + for(; i < length; ++i) { + if(outputBuff[i] == '\n') { + char *printBuff = malloc(i+2); + strncpy(printBuff, outputBuff, i+1); + printBuff[i+1] = '\0'; + printf("%s", printBuff); + free(printBuff); + /* now remove the printed string from the buffer + and start again */ + { + long a = 0; + ++i; + /* +1 to include \0 */ + for(; i < length + 1; ++a, ++i) { + outputBuff[a] = outputBuff[i]; + } + i = 0; + length = strlen(outputBuff); + } + } + } +} + +/* for printf, print all to a string. + Then cycle through all chars and see if \n is + written. If there is one, flush the output, otherwise + write to buffer */ +int printf_wrap(const char *format, ...) { + /* append output to buffer */ + va_list arglist; + int result; + va_start( arglist, format ); + result = vsprintf(outputBuff + strlen(outputBuff), format, arglist); + va_end( arglist ); + + /* Now flush if there is a reason to */ + flush_until_newline(); + + attachOutputFlush(); + + return result; +} + +int putchar_wrap(int c) { + long length = strlen(outputBuff); + outputBuff[length] = (char)c; + outputBuff[length+1] = '\0'; + if(c == '\n') { + flush_until_newline(); + } + + attachOutputFlush(); + + return c; +} + +int fflush_wrap(FILE* stream) { + if(stream == stdout) { + flush_all_output(); + } + return fflush(stream); +} + +int fclose_wrap(FILE* stream) { + if(stream == stdout) { + flush_all_output(); + } + return fclose(stream); +} +#endif /* BUFFERED_OUTPUT */ + /* Need input buffer to know whether we need another input request */ /* TODO allocate this dynamically */ static char inputBuff[1<<10] = ""; +static long scanf_wrap_number_read = 0; /* read remaining input into buffer so it can be used in next call */ void readIntoBuffer() { @@ -58,67 +159,63 @@ char checkInputRequest() { int scanf_wrap(const char *format, ...) { char doRequest = checkInputRequest(); - char *inputString = 0; + char *formatString = 0; if(doRequest) { +#ifdef BUFFERED_OUTPUT + flush_all_output(); +#endif printf(""); fflush(stdout); /* read everything from stdin into buffer */ readIntoBuffer(); } - /* make substring from inputBuff */ + /* add %n to format string to get number of written chars */ { - const long length = strlen(inputBuff); - long index = 0; - char leadingSpace = 1; - for(; index < length; ++index) { - if(isspace(inputBuff[index])) { - if(!leadingSpace) { - break; - } - } else { - leadingSpace = 0; - } - } - - inputString = malloc(index + 1); - strncpy(inputString, inputBuff, index); - inputString[index] = '\0'; - /* now move inputBuff up */ - { - long a = 0; - leadingSpace = 1; - /* +1 to include \0 */ - for(; index < length + 1; ++index) { - if(!leadingSpace || isspace(inputBuff[index]) == 0) { - leadingSpace = 0; - inputBuff[a] = inputBuff[index]; - ++a; - } - } - } + long length = strlen(format); + formatString = malloc(length + 3); + strcpy(formatString, format); + formatString[length] = '%'; + formatString[length + 1] = 'n'; + formatString[length + 2] = '\0'; } { va_list arglist; int result; va_start(arglist, format); - result = vsscanf(inputString, format, arglist); + result = vsscanf(inputBuff, formatString, arglist); va_end(arglist); - free(inputString); + /* now move inputBuff up or remove for c89 */ +#ifdef C89_SUPPORT + inputBuff[0] = '\0'; +#else /* C89_SUPPORT */ + { + const long length = strlen(inputBuff); + long index = scanf_wrap_number_read; + long a = 0; + /* +1 to include \0 */ + for(; index < length + 1; ++a, ++index) { + inputBuff[a] = inputBuff[index]; + } + } +#endif /* C89_SUPPORT */ + free(formatString); return result; } } - int getchar_wrap(){ /* check if there is still something in the input buffer*/ char input = 0; long length = strlen(inputBuff); if(length <= 0) { +#ifdef BUFFERED_OUTPUT + flush_all_output(); +#endif printf(""); fflush(stdout); @@ -145,7 +242,7 @@ int getchar_wrap(){ depending on the language version used */ #ifndef C89_SUPPORT /* Need double hashes in case there are no __VA_ARGS__*/ -#define scanf(format, ...) scanf_wrap(format, ##__VA_ARGS__) +#define scanf(format, ...) scanf_wrap(format, ##__VA_ARGS__, &scanf_wrap_number_read) #else /* C89_SUPPORT */ /* Since there are no variadic macros in C89, this is the only way although it is horrible */ @@ -154,110 +251,12 @@ int getchar_wrap(){ #define getchar() getchar_wrap() -/* output functions to replicate terminal behaviour */ +/* Output defines */ #ifdef BUFFERED_OUTPUT -/* buffer for all output */ -/* TODO allocate this dynamically */ -static char outputBuff[1<<10] = ""; -static char attachedOutputFlush = 0; - -void flush_all_output() { - printf("%s", outputBuff); - fflush(stdout); - outputBuff[0] = '\0'; -} - -/* Flush all output on exit */ -void attachOutputFlush() { - if(attachedOutputFlush == 0){ - int error = atexit(flush_all_output); - if(error != 0) { - fprintf(stderr, "ERROR: Could not set exit function! Error %d\n", error); - } - attachedOutputFlush = 1; - } -} - -/* this function is called to check whether there - is a '\n' in the output that should be flushed */ -void flush_until_newline() { - long i = 0; - long length = strlen(outputBuff); - for(; i < length; ++i) { - if(outputBuff[i] == '\n') { - char *printBuff = malloc(i+2); - strncpy(printBuff, outputBuff, i+1); - printBuff[i+1] = '\0'; - printf("%s", printBuff); - free(printBuff); - /* now remove the printed string from the buffer - and start again */ - { - long a = 0; - ++i; - /* +1 to include \0 */ - for(; i < length + 1; ++a, ++i) { - outputBuff[a] = outputBuff[i]; - } - i = 0; - length = strlen(outputBuff); - } - } - } -} - -/* for printf, print all to a string. - Then cycle through all chars and see if \n is - written. If there is one, flush the output, otherwise - write to buffer */ -int printf_wrap(const char *format, ...) { - /* append output to buffer */ - va_list arglist; - int result; - va_start( arglist, format ); - result = vsprintf(outputBuff + strlen(outputBuff), format, arglist); - va_end( arglist ); - - /* Now flush if there is a reason to */ - flush_until_newline(); - - attachOutputFlush(); - - return result; -} - -int putchar_wrap(int c) { - long length = strlen(outputBuff); - outputBuff[length] = (char)c; - outputBuff[length+1] = '\0'; - if(c == '\n') { - flush_until_newline(); - } - - attachOutputFlush(); - - return c; -} - -int fflush_wrap(FILE* stream) { - if(stream == stdout) { - flush_all_output(); - } - return fflush(stream); -} - -int fclose_wrap(FILE* stream) { - if(stream == stdout) { - flush_all_output(); - } - return fclose(stream); -} - #define printf printf_wrap #define putchar putchar_wrap #define fflush fflush_wrap #define fclose fclose_wrap - #endif /* BUFFERED_OUTPUT */ /* Replace FILE write operations for read-only systems */ From 4f8d436439eebfbf729efb6b6577fc2ec3311ab3 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Tue, 12 May 2020 12:28:40 +0200 Subject: [PATCH 11/16] Fixed scanf for C89. Small improvements for newer standards. --- jupyter_c_kernel/resources/stdio_wrap.h | 48 +++++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index 258f37b..22584d1 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -137,7 +137,10 @@ void readIntoBuffer() { /* check whether input request is needed */ char checkInputRequest() { - return strlen(inputBuff) <= 0; + const long length = strlen(inputBuff); + long i = 0; + for(; i < length && isspace(inputBuff[i]); ++i); + return i == length; } /* Define the input functions to overload the old ones */ @@ -155,6 +158,35 @@ char checkInputRequest() { extern int vsscanf (const char *__restrict __s, const char *__restrict __format, _G_va_list __arg) __THROW __attribute__ ((__format__ (__scanf__, 2, 0))); + + /* replace all % with %* to suppress read in and do test run */ + long find_scanf_length(const char *format) { + const long length = strlen(format); + /* allow for maximum of 50 format specifiers */ + char *formatString = malloc(length + 53); + long index = 0; + long formatIndex = 0; + for(; index < length; ++index, ++formatIndex) { + formatString[formatIndex] = format[index]; + if(format[index] == '%' && + (index + 1 < length && format[index + 1] != '%')) { + formatString[++formatIndex] = '*'; + } + } + /* add number readin */ + formatString[formatIndex++] = '%'; + formatString[formatIndex++] = 'n'; + formatString[formatIndex] = '\0'; + + /* now run and record how many characters were read */ + { + int readLength = 0; + sscanf(inputBuff, formatString, &readLength); + free(formatString); + + return readLength; + } + } #endif /* C89_SUPPORT */ int scanf_wrap(const char *format, ...) { @@ -173,12 +205,18 @@ int scanf_wrap(const char *format, ...) { /* add %n to format string to get number of written chars */ { - long length = strlen(format); + const long length = strlen(format); formatString = malloc(length + 3); strcpy(formatString, format); +#ifndef C89_SUPPORT formatString[length] = '%'; formatString[length + 1] = 'n'; formatString[length + 2] = '\0'; +#else /* C89_SUPPORT */ + formatString[length] = '\0'; + /* In C89 we need to find how far scanf will read, by hand */ + scanf_wrap_number_read = find_scanf_length(format); +#endif /* C89_SUPPORT */ } { @@ -188,10 +226,7 @@ int scanf_wrap(const char *format, ...) { result = vsscanf(inputBuff, formatString, arglist); va_end(arglist); - /* now move inputBuff up or remove for c89 */ -#ifdef C89_SUPPORT - inputBuff[0] = '\0'; -#else /* C89_SUPPORT */ + /* now move inputBuff up */ { const long length = strlen(inputBuff); long index = scanf_wrap_number_read; @@ -201,7 +236,6 @@ int scanf_wrap(const char *format, ...) { inputBuff[a] = inputBuff[index]; } } -#endif /* C89_SUPPORT */ free(formatString); return result; From 66cbe9b2ca111c730ccbeecfd0d1fe8c2ef78b90 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Thu, 25 Feb 2021 12:50:14 +0100 Subject: [PATCH 12/16] Set bufferedOutput=True since that is the usual command line behaviour. Updated README. --- README.md | 29 +++++++++++++++++++++-------- jupyter_c_kernel/kernel.py | 7 +++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 47912c6..e7a3d06 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,25 @@ -# Minimal C kernel for Jupyter +# C kernel for Jupyter + +This project was forked from [https://github.com/brendan-rius/jupyter-c-kernel](brendan-rius/jupyter-c-kernel) as that project seems to have been abandoned. (PR is pending) + +This project includes fixes to many issues reported in [https://github.com/brendan-rius/jupyter-c-kernel](brendan-rius/jupyter-c-kernel), as well as the following additional features: + +* Option for buffered output to mimic command line behaviour (useful for teaching, default is on) +* Command line input via `scanf` and `getchar` +* Support for `C89`/`ANSI C` (all newer versions were already supported and still are) + +Following limitations compared to command line execution exist: + +* Input is always buffered due to limitations of the jupyter interface +* When using `-ansi` or `-std=C89`, glibc still has to support at least `C99` for the interfacing with jupyter (this should not be an issue on an OS made after 2000) ## Use with Docker (recommended) - * `docker pull xaverklemenschits/jupyter-c-kernel` - * `docker run -p 8888:8888 xaverklemenschits/jupyter-c-kernel` - * Copy the given URL containing the token, and browse to it. For instance: +* `docker pull xaverklemenschits/jupyter-c-kernel` +* `docker run -p 8888:8888 xaverklemenschits/jupyter-c-kernel` +* Copy the given URL containing the token, and browse to it. For instance: - ``` + ```bash Copy/paste this URL into your browser when you connect for the first time, to login with a token: http://localhost:8888/?token=66750c80bd0788f6ba15760aadz53beb9a9fb4cf8ac15ce8 @@ -16,14 +29,14 @@ Works only on Linux and OS X. Windows is not supported yet. If you want to use this project on Windows, please use Docker. - - * Make sure you have the following requirements installed: +* Make sure you have the following requirements installed: * gcc * jupyter * python 3 * pip -### Step-by-step: +### Step-by-step + ```bash git clone https://github.com/XaverKlemenschits/jupyter-c-kernel.git cd jupyter-c-kernel diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 44d3095..59162d7 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -106,7 +106,7 @@ def __init__(self, *args, **kwargs): super(CKernel, self).__init__(*args, **kwargs) self._allow_stdin = True self.readOnlyFileSystem = False - self.bufferedOutput = False + self.bufferedOutput = True self.linkMaths = True # always link math library self.wAll = True # show all warnings by default self.wError = False # but keep comipiling for warnings @@ -191,7 +191,10 @@ def _filter_magics(self, code): for argument in re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', value): magics['args'] += [argument.strip('"')] - # only keep lines which did not contain magics + # always add empty line, so line numbers don't change + actualCode += '\n' + + # keep lines which did not contain magics else: actualCode += line + '\n' From ddce20b4cacd4f6f2471a34a5722dad8c86c0eaa Mon Sep 17 00:00:00 2001 From: Xaver K Date: Wed, 26 Oct 2022 17:29:32 +0200 Subject: [PATCH 13/16] Fixed crash when magic line is empty. (#4) --- jupyter_c_kernel/kernel.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 59162d7..43c660a 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -180,7 +180,12 @@ def _filter_magics(self, code): for line in code.splitlines(): if line.startswith('//%'): - key, value = line[3:].split(":", 2) + magicSplit = line[3:].split(":", 2) + if(len(magicSplit) < 2): + self._write_to_stderr("[C kernel] Magic line starting with '//%' is missing a semicolon, ignoring.") + continue + + key, value = magicSplit key = key.strip().lower() if key in ['ldflags', 'cflags']: From ca79b349d99fba0b4d312ca0b107a0ca92790e5d Mon Sep 17 00:00:00 2001 From: Xaver K Date: Wed, 26 Oct 2022 18:04:01 +0200 Subject: [PATCH 14/16] Added support for changing the standard using a magic. (#5) --- jupyter_c_kernel/kernel.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 43c660a..794d9e6 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -110,6 +110,7 @@ def __init__(self, *args, **kwargs): self.linkMaths = True # always link math library self.wAll = True # show all warnings by default self.wError = False # but keep comipiling for warnings + self.standard = "c11" # default standard if none is specified self.files = [] mastertemp = tempfile.mkstemp(suffix='.out') os.close(mastertemp[0]) @@ -152,11 +153,7 @@ def create_jupyter_subprocess(self, cmd): self._read_from_stdin) def compile_with_gcc(self, source_filename, binary_filename, cflags=None, ldflags=None): - # cflags = ['-std=c89', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags - # cflags = ['-std=c99', '-Wdeclaration-after-statement', '-Wvla', '-fPIC', '-shared', '-rdynamic'] + cflags - # cflags = ['-std=iso9899:199409', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags - # cflags = ['-std=c99', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags - cflags = ['-std=c11', '-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags + cflags = ['-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags if self.linkMaths: cflags = cflags + ['-lm'] if self.wError: @@ -203,6 +200,10 @@ def _filter_magics(self, code): else: actualCode += line + '\n' + # add default standard if cflags does not contain one + if not any(item.startswith('-std=') for item in magics["cflags"]): + magics["cflags"] += ["-std=" + self.standard] + return magics, actualCode # check whether int main() is specified, if not add it around the code From 5af96beec6a9266d6c77b775c37cd7fa86227568 Mon Sep 17 00:00:00 2001 From: Jiho Park Date: Tue, 21 May 2024 03:25:15 +0900 Subject: [PATCH 15/16] Fix language info name to `c` to get correct syntax highlighting. (#8) --- jupyter_c_kernel/kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 794d9e6..42dc854 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -90,7 +90,7 @@ class CKernel(Kernel): implementation_version = '1.0' language = 'c' language_version = 'C11' - language_info = {'name': 'text/x-csrc', + language_info = {'name': 'c', 'mimetype': 'text/x-csrc', 'file_extension': '.c'} banner = "C kernel.\n" \ From e88cb5f36b53c102968456b0f1c563a4694c496e Mon Sep 17 00:00:00 2001 From: marjohnsen <90933559+marjohnsen@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:04:56 +0100 Subject: [PATCH 16/16] Add void to empty parameter list (#9) --- jupyter_c_kernel/resources/stdio_wrap.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h index 22584d1..7b87d26 100644 --- a/jupyter_c_kernel/resources/stdio_wrap.h +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -125,7 +125,7 @@ static char inputBuff[1<<10] = ""; static long scanf_wrap_number_read = 0; /* read remaining input into buffer so it can be used in next call */ -void readIntoBuffer() { +void readIntoBuffer(void) { long length = strlen(inputBuff); char nextChar = 0; while((nextChar = getchar()) != '\n' && nextChar != EOF){ @@ -136,7 +136,7 @@ void readIntoBuffer() { } /* check whether input request is needed */ -char checkInputRequest() { +char checkInputRequest(void) { const long length = strlen(inputBuff); long i = 0; for(; i < length && isspace(inputBuff[i]); ++i); @@ -242,7 +242,7 @@ int scanf_wrap(const char *format, ...) { } } -int getchar_wrap(){ +int getchar_wrap(void){ /* check if there is still something in the input buffer*/ char input = 0; long length = strlen(inputBuff);