diff --git a/.circleci/config.yml b/.circleci/config.yml index e045d93..c4ed6a4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ jobs: pkg-config python3-dev python3-setuptools - - run: git clone --depth=1 https://github.com/TokTok/c-toxcore + - run: git clone --recursive --depth=1 https://github.com/TokTok/c-toxcore - run: cd c-toxcore; . .github/scripts/flags-gcc.sh; add_flag -fsanitize=address; @@ -44,10 +44,10 @@ jobs: -DTRACE=ON -DMUST_BUILD_TOXAV=ON - run: cd c-toxcore/_build && ninja install -j$(nproc) -# - run: -# export CFLAGS="-I$PWD/c-toxcore/_install/include -fsanitize=address"; -# export LDFLAGS="-L$PWD/c-toxcore/_install/lib -Wl,-rpath,$PWD/c-toxcore/_install/lib"; -# python3 setup.py build_ext --inplace + - run: + export CFLAGS="-I$PWD/c-toxcore/_install/include -fsanitize=address"; + export LDFLAGS="-L$PWD/c-toxcore/_install/lib -Wl,-rpath,$PWD/c-toxcore/_install/lib"; + python3 setup.py build_ext --inplace # - run: ASAN_OPTIONS=detect_leaks=0 # LD_PRELOAD=libasan.so.5 # PYTHONPATH=. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d7f7a42 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.mypy_cache diff --git a/BUILD.bazel b/BUILD.bazel index e4b10db..961de80 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -5,23 +5,59 @@ package(features = ["layering_check"]) project() +SUBSYSTEMS = [ + "log", + "memory", + "network", + "options", + "random", + "system", + "time", +] + +[genrule( + name = "pytox/" + sys, + srcs = [ + "pytox/src/%s.pxd" % sys, + "pytox/src/%s.pyx" % sys, + "//c-toxcore:public", + ], + outs = [ + "pytox/%s.pxd" % sys, + "pytox/%s.pyx" % sys, + ], + cmd = " ".join([ + "$(location //py_toxcore_c/tools:gen_api)", + "$(location pytox/src/%s.pyx)" % sys, + "$(GENDIR)/c-toxcore", + "> $(location pytox/%s.pyx);" % sys, + "$(location //py_toxcore_c/tools:gen_api)", + "$(location pytox/src/%s.pxd)" % sys, + "$(GENDIR)/c-toxcore", + "> $(location pytox/%s.pxd)" % sys, + ]), + tools = ["//py_toxcore_c/tools:gen_api"], +) for sys in SUBSYSTEMS] + genrule( name = "pytox/core", srcs = [ "pytox/src/core.pyx", - "//c-toxcore:tox/tox.h", + "//c-toxcore:tox/toxcore/tox.h", ], outs = ["pytox/core.pyx"], - cmd = "$(location //py_toxcore_c/tools:gen_api) $(location pytox/src/core.pyx) $(location //c-toxcore:tox/tox.h) > $@", + cmd = "$(location //py_toxcore_c/tools:gen_api) $(location pytox/src/core.pyx) $(GENDIR)/c-toxcore > $@", tools = ["//py_toxcore_c/tools:gen_api"], ) pyx_library( name = "pytox", srcs = [ + "pytox.pxd", "pytox/av.pyx", "pytox/core.pyx", - ], + "pytox/error.pyx", + ] + ["pytox/%s.pxd" % sys for sys in SUBSYSTEMS] + ["pytox/%s.pyx" % sys for sys in SUBSYSTEMS], cdeps = ["//c-toxcore"], cython_directives = {"language_level": "3"}, visibility = ["//visibility:public"], diff --git a/Dockerfile b/Dockerfile index fdde7fd..3c6506a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,10 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 LABEL maintainer="iphydf@gmail.com" RUN apt-get update \ && DEBIAN_FRONTEND="noninteractive" apt-get install --no-install-recommends -y \ ca-certificates \ cmake \ - cython \ gcc \ g++ \ git \ @@ -19,23 +18,47 @@ RUN apt-get update \ python3-pip \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ - && pip3 install mypy + && pip3 install cython mypy WORKDIR /build -RUN git clone --depth=1 https://github.com/TokTok/c-toxcore /build/c-toxcore \ +RUN git clone --recursive --depth=1 --branch=system https://github.com/iphydf/c-toxcore /build/c-toxcore \ && cmake -GNinja -B/build/c-toxcore/_build -H/build/c-toxcore \ -DBOOTSTRAP_DAEMON=OFF \ -DENABLE_STATIC=OFF \ -DMUST_BUILD_TOXAV=ON \ && cmake --build /build/c-toxcore/_build --target install --parallel "$(nproc)" \ - && ldconfig + && ldconfig && echo 2 -COPY pytox /build/pytox +# Tools first, they change less. COPY tools /build/tools +COPY pytox.pxd /build/ +COPY pytox /build/pytox RUN mypy --strict tools/gen_api.py \ - && tools/gen_api.py pytox/src/core.pyx /usr/local/include/tox/tox.h > pytox/core.pyx \ - && cython pytox/av.pyx pytox/core.pyx + && tools/gen_api.py pytox/src/core.pyx /usr/local/include > pytox/core.pyx \ + && tools/gen_api.py pytox/src/log.pxd /usr/local/include > pytox/log.pxd \ + && tools/gen_api.py pytox/src/log.pyx /usr/local/include > pytox/log.pyx \ + && tools/gen_api.py pytox/src/memory.pxd /usr/local/include > pytox/memory.pxd \ + && tools/gen_api.py pytox/src/memory.pyx /usr/local/include > pytox/memory.pyx \ + && tools/gen_api.py pytox/src/network.pxd /usr/local/include > pytox/network.pxd \ + && tools/gen_api.py pytox/src/network.pyx /usr/local/include > pytox/network.pyx \ + && tools/gen_api.py pytox/src/options.pxd /usr/local/include > pytox/options.pxd \ + && tools/gen_api.py pytox/src/options.pyx /usr/local/include > pytox/options.pyx \ + && tools/gen_api.py pytox/src/random.pxd /usr/local/include > pytox/random.pxd \ + && tools/gen_api.py pytox/src/random.pyx /usr/local/include > pytox/random.pyx \ + && tools/gen_api.py pytox/src/system.pxd /usr/local/include > pytox/system.pxd \ + && tools/gen_api.py pytox/src/system.pyx /usr/local/include > pytox/system.pyx \ + && tools/gen_api.py pytox/src/time.pxd /usr/local/include > pytox/time.pxd \ + && tools/gen_api.py pytox/src/time.pyx /usr/local/include > pytox/time.pyx \ + && cython -I $PWD -X "language_level=3" --line-directives pytox/av.pyx pytox/core.pyx \ + pytox/error.pyx \ + pytox/log.pyx \ + pytox/memory.pyx \ + pytox/network.pyx \ + pytox/options.pyx \ + pytox/random.pyx \ + pytox/system.pyx \ + pytox/time.pyx COPY setup.py /build/ RUN python3 setup.py install \ diff --git a/pytox.pxd b/pytox.pxd new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pytox.pxd @@ -0,0 +1 @@ + diff --git a/pytox/.gitignore b/pytox/.gitignore new file mode 100644 index 0000000..9a7ca08 --- /dev/null +++ b/pytox/.gitignore @@ -0,0 +1,14 @@ +/core.pxd +/core.pyx +/log.pxd +/log.pyx +/memory.pxd +/memory.pyx +/network.pxd +/network.pyx +/random.pxd +/random.pyx +/system.pxd +/system.pyx +/time.pxd +/time.pyx diff --git a/pytox/error.pyx b/pytox/error.pyx new file mode 100644 index 0000000..cd8fff7 --- /dev/null +++ b/pytox/error.pyx @@ -0,0 +1,15 @@ +class UseAfterFreeException(Exception): + def __init__(self): + super().__init__( + "object used after it was killed/freed (or it was never initialised)") + +class ToxException(Exception): + pass + +class ApiException(ToxException): + def __init__(self, err): + super().__init__(err) + self.error = err + +class LengthException(ToxException): + pass diff --git a/pytox/src/core.pyx b/pytox/src/core.pyx index c9894e1..e0324a2 100644 --- a/pytox/src/core.pyx +++ b/pytox/src/core.pyx @@ -6,7 +6,11 @@ from libcpp cimport bool from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t from libc.stdlib cimport malloc, free -cdef extern from "tox/tox.h": pass +from pytox import error +from pytox.options cimport Tox_Options, ToxOptions +from pytox.system cimport Tox_System + +cdef extern from "tox/toxcore/tox.h": pass VERSION: str = "%d.%d.%d" % (tox_version_major(), tox_version_minor(), tox_version_patch()) @@ -28,72 +32,22 @@ MAX_FILENAME_LENGTH: int = tox_max_filename_length() MAX_HOSTNAME_LENGTH: int = tox_max_hostname_length() -class UseAfterFreeException(Exception): - def __init__(self): - super().__init__( - "object used after it was killed/freed (or it was never initialised)") - -class ToxException(Exception): - pass - -class ApiException(ToxException): - def __init__(self, err): - super().__init__(err) - self.error = err - -class LengthException(ToxException): - pass - cdef void _check_len(str name, bytes data, int expected_length) except *: if len(data) != expected_length: - raise LengthException( + raise error.LengthException( f"parameter '{name}' received bytes of invalid" f"length {len(data)}, expected {expected_length}") - -cdef class ToxOptions: - - cdef Tox_Options *_ptr - - def __init__(self): - cdef Tox_Err_Options_New err = TOX_ERR_OPTIONS_NEW_OK - self._ptr = tox_options_new(&err) - if err: raise ApiException(Tox_Err_Options_New(err)) - - def __dealloc__(self): - self.__exit__(None, None, None) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - tox_options_free(self._ptr) - self._ptr = NULL - - cdef Tox_Options *_get(self) except *: - if self._ptr is NULL: - raise UseAfterFreeException() - return self._ptr - - @property - def ipv6_enabled(self) -> bool: - return tox_options_get_ipv6_enabled(self._get()) - - @ipv6_enabled.setter - def ipv6_enabled(self, value: bool): - tox_options_set_ipv6_enabled(self._get(), value) - - cdef class Core: cdef Tox *_ptr def __init__(self, options: ToxOptions = None): - cdef Tox_Err_New err = TOX_ERR_NEW_OK + err = TOX_ERR_NEW_OK if options is None: options = ToxOptions() - self._ptr = tox_new(options._ptr, &err) - if err: raise ApiException(Tox_Err_New(err)) + self._ptr = tox_new(options._get(), &err) + if err: raise error.ApiException(Tox_Err_New(err)) install_handlers(self._get()) @@ -109,13 +63,13 @@ cdef class Core: cdef Tox *_get(self) except *: if self._ptr is NULL: - raise UseAfterFreeException() + raise error.UseAfterFreeException() return self._ptr @property def savedata(self) -> bytes: - cdef size_t size = tox_get_savedata_size(self._get()) - cdef uint8_t *data = malloc(size * sizeof(uint8_t)) + size = tox_get_savedata_size(self._get()) + data = malloc(size * sizeof(uint8_t)) try: tox_get_savedata(self._get(), data) return data[:size] @@ -124,7 +78,7 @@ cdef class Core: def bootstrap(self, host: str, port: int, public_key: bytes) -> bool: _check_len("public_key", public_key, tox_public_key_size()) - cdef Tox_Err_Bootstrap err = TOX_ERR_BOOTSTRAP_OK + err = TOX_ERR_BOOTSTRAP_OK return tox_bootstrap(self._get(), host.encode("utf-8"), port, public_key, &err) @property @@ -143,8 +97,8 @@ cdef class Core: @property def address(self) -> bytes: - cdef size_t size = tox_address_size() - cdef uint8_t *data = malloc(size * sizeof(uint8_t)) + size = tox_address_size() + data = malloc(size * sizeof(uint8_t)) try: tox_self_get_address(self._get(), data) return data[:size] @@ -157,8 +111,8 @@ cdef class Core: @property def public_key(self) -> bytes: - cdef size_t size = tox_public_key_size() - cdef uint8_t *data = malloc(size * sizeof(uint8_t)) + size = tox_public_key_size() + data = malloc(size * sizeof(uint8_t)) try: tox_self_get_public_key(self._get(), data) return data[:tox_public_key_size()] @@ -167,8 +121,8 @@ cdef class Core: @property def secret_key(self) -> bytes: - cdef size_t size = tox_secret_key_size() - cdef uint8_t *data = malloc(size * sizeof(uint8_t)) + size = tox_secret_key_size() + data = malloc(size * sizeof(uint8_t)) try: tox_self_get_secret_key(self._get(), data) return data[:tox_secret_key_size()] @@ -177,8 +131,8 @@ cdef class Core: @property def name(self) -> bytes: - cdef size_t size = tox_self_get_name_size(self._get()) - cdef uint8_t *data = malloc(size * sizeof(uint8_t)) + size = tox_self_get_name_size(self._get()) + data = malloc(size * sizeof(uint8_t)) try: tox_self_get_name(self._get(), data) return data[:size] @@ -187,14 +141,14 @@ cdef class Core: @name.setter def name(self, name: bytes) -> None: - cdef Tox_Err_Set_Info err = TOX_ERR_SET_INFO_OK + err = TOX_ERR_SET_INFO_OK tox_self_set_name(self._get(), name, len(name), &err) - if err: raise ApiException(Tox_Err_Set_Info(err)) + if err: raise error.ApiException(Tox_Err_Set_Info(err)) @property def status_message(self) -> bytes: - cdef size_t size = tox_self_get_status_message_size(self._get()) - cdef uint8_t *data = malloc(size * sizeof(uint8_t)) + size = tox_self_get_status_message_size(self._get()) + data = malloc(size * sizeof(uint8_t)) try: tox_self_get_status_message(self._get(), data) return data[:size] @@ -203,9 +157,9 @@ cdef class Core: @status_message.setter def status_message(self, status_message: bytes) -> None: - cdef Tox_Err_Set_Info err = TOX_ERR_SET_INFO_OK + err = TOX_ERR_SET_INFO_OK tox_self_set_status_message(self._get(), status_message, len(status_message), &err) - if err: raise ApiException(Tox_Err_Set_Info(err)) + if err: raise error.ApiException(Tox_Err_Set_Info(err)) @property def status(self) -> Tox_User_Status: @@ -217,17 +171,17 @@ cdef class Core: def friend_add(self, address: bytes, message: bytes): _check_len("address", address, tox_address_size()) - cdef Tox_Err_Friend_Add err = TOX_ERR_FRIEND_ADD_OK + err = TOX_ERR_FRIEND_ADD_OK tox_friend_add(self._get(), address, message, len(message), &err) - if err: raise ApiException(Tox_Err_Friend_Add(err)) + if err: raise error.ApiException(Tox_Err_Friend_Add(err)) def friend_add_norequest(self, public_key: bytes): _check_len("public_key", public_key, tox_public_key_size()) - cdef Tox_Err_Friend_Add err = TOX_ERR_FRIEND_ADD_OK + err = TOX_ERR_FRIEND_ADD_OK tox_friend_add_norequest(self._get(), public_key, &err) - if err: raise ApiException(Tox_Err_Friend_Add(err)) + if err: raise error.ApiException(Tox_Err_Friend_Add(err)) def friend_delete(self, friend_number: int): - cdef Tox_Err_Friend_Delete err = TOX_ERR_FRIEND_DELETE_OK + err = TOX_ERR_FRIEND_DELETE_OK tox_friend_delete(self._get(), friend_number, &err) - if err: raise ApiException(Tox_Err_Friend_Delete(err)) + if err: raise error.ApiException(Tox_Err_Friend_Delete(err)) diff --git a/pytox/src/log.pxd b/pytox/src/log.pxd new file mode 100644 index 0000000..5ee8000 --- /dev/null +++ b/pytox/src/log.pxd @@ -0,0 +1,18 @@ +# cython: language_level=3 +cdef extern from "tox/toxcore/tox_log.h": + ctypedef struct Tox_Log + + cpdef enum Tox_Log_Level: + TOX_LOG_LEVEL_TRACE, + TOX_LOG_LEVEL_DEBUG, + TOX_LOG_LEVEL_INFO, + TOX_LOG_LEVEL_WARNING, + TOX_LOG_LEVEL_ERROR, + +cdef extern from "tox/toxcore/os_log.h": pass + +cdef class ToxLog: + + cdef Tox_Log *_ptr + + cdef Tox_Log *_get(self) except * diff --git a/pytox/src/log.pyx b/pytox/src/log.pyx new file mode 100644 index 0000000..9df476a --- /dev/null +++ b/pytox/src/log.pyx @@ -0,0 +1,35 @@ +# cython: language_level=3 +"""Toxcore bindings. +""" + +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t + +from pytox import error +from pytox cimport memory +from pytox.memory cimport Tox_Memory + +cdef extern from "tox/toxcore/tox_log.h": pass + + +cdef class ToxLog: + """Logger.""" + + def __init__(self, memory.ToxMemory mem): + self._ptr = tox_log_new(NULL, self, mem._get()) + if self._ptr is NULL: raise MemoryError("tox_log_new") + + def __dealloc__(self): + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + tox_log_free(self._ptr) + self._ptr = NULL + + cdef Tox_Log *_get(self) except *: + if self._ptr is NULL: + raise error.UseAfterFreeException() + return self._ptr diff --git a/pytox/src/memory.pxd b/pytox/src/memory.pxd new file mode 100644 index 0000000..666bfac --- /dev/null +++ b/pytox/src/memory.pxd @@ -0,0 +1,10 @@ +cdef extern from "tox/toxcore/tox_memory.h": + ctypedef struct Tox_Memory + +cdef extern from "tox/toxcore/os_memory.h": pass + +cdef class ToxMemory: + + cdef Tox_Memory *_ptr + + cdef Tox_Memory *_get(self) except * diff --git a/pytox/src/memory.pyx b/pytox/src/memory.pyx new file mode 100644 index 0000000..3fa59c4 --- /dev/null +++ b/pytox/src/memory.pyx @@ -0,0 +1,32 @@ +# cython: language_level=3 +"""Toxcore bindings. +""" + +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t + +from pytox import error + +cdef extern from "tox/toxcore/tox_memory.h": pass + + +cdef class ToxMemory: + + def __init__(self): + self._ptr = tox_memory_new(NULL, self) + if self._ptr is NULL: raise MemoryError("tox_memory_new") + + def __dealloc__(self): + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + tox_memory_free(self._ptr) + self._ptr = NULL + + cdef Tox_Memory *_get(self) except *: + if self._ptr is NULL: + raise error.UseAfterFreeException() + return self._ptr diff --git a/pytox/src/network.pxd b/pytox/src/network.pxd new file mode 100644 index 0000000..4fb4a78 --- /dev/null +++ b/pytox/src/network.pxd @@ -0,0 +1,10 @@ +cdef extern from "tox/toxcore/tox_network.h": + ctypedef struct Tox_Network + +cdef extern from "tox/toxcore/os_network.h": pass + +cdef class ToxNetwork: + + cdef Tox_Network *_ptr + + cdef Tox_Network *_get(self) except * diff --git a/pytox/src/network.pyx b/pytox/src/network.pyx new file mode 100644 index 0000000..6d6dbf8 --- /dev/null +++ b/pytox/src/network.pyx @@ -0,0 +1,34 @@ +# cython: language_level=3 +"""Toxcore bindings. +""" + +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t + +from pytox import error +from pytox cimport memory +from pytox.memory cimport Tox_Memory + +cdef extern from "tox/toxcore/tox_network.h": pass + + +cdef class ToxNetwork: + + def __init__(self, memory.ToxMemory mem): + self._ptr = tox_network_new(NULL, self, mem._get()) + if self._ptr is NULL: raise MemoryError("tox_network_new") + + def __dealloc__(self): + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + tox_network_free(self._ptr) + self._ptr = NULL + + cdef Tox_Network *_get(self) except *: + if self._ptr is NULL: + raise error.UseAfterFreeException() + return self._ptr diff --git a/pytox/src/options.pxd b/pytox/src/options.pxd new file mode 100644 index 0000000..7161fd7 --- /dev/null +++ b/pytox/src/options.pxd @@ -0,0 +1,8 @@ +cdef extern from "tox/toxcore/tox_options.h": + cdef struct Tox_Options + +cdef class ToxOptions: + + cdef Tox_Options *_ptr + + cdef Tox_Options *_get(self) except * diff --git a/pytox/src/options.pyx b/pytox/src/options.pyx new file mode 100644 index 0000000..5550170 --- /dev/null +++ b/pytox/src/options.pyx @@ -0,0 +1,43 @@ +# cython: language_level=3 +"""Toxcore bindings. +""" + +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t + +from pytox import error +from pytox.log cimport Tox_Log_Level +from pytox.system cimport Tox_System + +cdef extern from "tox/toxcore/tox_options.h": pass + + +cdef class ToxOptions: + + def __init__(self): + err = TOX_ERR_OPTIONS_NEW_OK + self._ptr = tox_options_new(&err) + if err: raise error.ApiException(Tox_Err_Options_New(err)) + + def __dealloc__(self): + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + tox_options_free(self._ptr) + self._ptr = NULL + + cdef Tox_Options *_get(self) except *: + if self._ptr is NULL: + raise error.UseAfterFreeException() + return self._ptr + + @property + def ipv6_enabled(self) -> bool: + return tox_options_get_ipv6_enabled(self._get()) + + @ipv6_enabled.setter + def ipv6_enabled(self, value: bool): + tox_options_set_ipv6_enabled(self._get(), value) diff --git a/pytox/src/random.pxd b/pytox/src/random.pxd new file mode 100644 index 0000000..99cad73 --- /dev/null +++ b/pytox/src/random.pxd @@ -0,0 +1,10 @@ +cdef extern from "tox/toxcore/tox_random.h": + ctypedef struct Tox_Random + +cdef extern from "tox/toxcore/os_random.h": pass + +cdef class ToxRandom: + + cdef Tox_Random *_ptr + + cdef Tox_Random *_get(self) except * diff --git a/pytox/src/random.pyx b/pytox/src/random.pyx new file mode 100644 index 0000000..2e830e4 --- /dev/null +++ b/pytox/src/random.pyx @@ -0,0 +1,34 @@ +# cython: language_level=3 +"""Toxcore bindings. +""" + +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t + +from pytox import error +from pytox cimport memory +from pytox.memory cimport Tox_Memory + +cdef extern from "tox/toxcore/tox_random.h": pass + + +cdef class ToxRandom: + + def __init__(self, memory.ToxMemory mem): + self._ptr = tox_random_new(NULL, self, mem._get()) + if self._ptr is NULL: raise MemoryError("tox_random_new") + + def __dealloc__(self): + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + tox_random_free(self._ptr) + self._ptr = NULL + + cdef Tox_Random *_get(self) except *: + if self._ptr is NULL: + raise error.UseAfterFreeException() + return self._ptr diff --git a/pytox/src/system.pxd b/pytox/src/system.pxd new file mode 100644 index 0000000..35039f4 --- /dev/null +++ b/pytox/src/system.pxd @@ -0,0 +1,16 @@ +from pytox.log cimport Tox_Log +from pytox.memory cimport Tox_Memory +from pytox.network cimport Tox_Network +from pytox.random cimport Tox_Random +from pytox.time cimport Tox_Time + +cdef extern from "tox/toxcore/tox_system.h": + ctypedef struct Tox_System + +cdef extern from "tox/toxcore/os_system.h": pass + +cdef class ToxSystem: + + cdef Tox_System *_ptr + + cdef Tox_System *_get(self) except * diff --git a/pytox/src/system.pyx b/pytox/src/system.pyx new file mode 100644 index 0000000..43be6ef --- /dev/null +++ b/pytox/src/system.pyx @@ -0,0 +1,43 @@ +# cython: language_level=3 +"""Toxcore bindings. +""" + +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t + +from pytox import error +from pytox.log cimport Tox_Log +from pytox.memory cimport Tox_Memory +from pytox.network cimport Tox_Network +from pytox.random cimport Tox_Random +from pytox.time cimport Tox_Time + +cdef extern from "tox/toxcore/tox_system.h": pass + + +cdef class ToxSystem: + + def __init__(self): + sys = os_system() + self._ptr = tox_system_new( + tox_system_get_log(sys), + tox_system_get_memory(sys), + tox_system_get_network(sys), + tox_system_get_random(sys), + tox_system_get_time(sys)) + if self._ptr is NULL: raise MemoryError("tox_system_new") + + def __dealloc__(self): + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + tox_system_free(self._ptr) + self._ptr = NULL + + cdef Tox_System *_get(self) except *: + if self._ptr is NULL: + raise error.UseAfterFreeException() + return self._ptr diff --git a/pytox/src/time.pxd b/pytox/src/time.pxd new file mode 100644 index 0000000..07f9841 --- /dev/null +++ b/pytox/src/time.pxd @@ -0,0 +1,8 @@ +cdef extern from "tox/toxcore/tox_time.h": + ctypedef struct Tox_Time + +cdef class ToxTime: + + cdef Tox_Time *_ptr + + cdef Tox_Time *_get(self) except * diff --git a/pytox/src/time.pyx b/pytox/src/time.pyx new file mode 100644 index 0000000..5f5fb31 --- /dev/null +++ b/pytox/src/time.pyx @@ -0,0 +1,34 @@ +# cython: language_level=3 +"""Toxcore bindings. +""" + +from libcpp cimport bool +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t + +from pytox import error +from pytox cimport memory +from pytox.memory cimport Tox_Memory + +cdef extern from "tox/toxcore/tox_time.h": pass + + +cdef class ToxTime: + + def __init__(self, memory.ToxMemory mem): + self._ptr = tox_time_new(NULL, self, mem._get()) + if self._ptr is NULL: raise MemoryError("tox_time_new") + + def __dealloc__(self): + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + tox_time_free(self._ptr) + self._ptr = NULL + + cdef Tox_Time *_get(self) except *: + if self._ptr is NULL: + raise error.UseAfterFreeException() + return self._ptr diff --git a/setup.py b/setup.py index e0683fc..7a48674 100644 --- a/setup.py +++ b/setup.py @@ -17,14 +17,22 @@ url="http://github.com/TokTok/py-toxcore-c", license="GPL", ext_modules=[ - Extension("pytox.av", ["pytox/av.c"], - extra_compile_args=cflags, - libraries=libraries), Extension( - "pytox.core", - ["pytox/core.c"], + f"pytox.{mod}", + [f"pytox/{mod}.c"], extra_compile_args=cflags, libraries=libraries, - ), + ) for mod in ( + "av", + "core", + "error", + "log", + "memory", + "network", + "options", + "random", + "system", + "time", + ) ], ) diff --git a/test/core_test.py b/test/core_test.py index ca272fd..dc3d648 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -1,6 +1,8 @@ import unittest from pytox import core +from pytox import error +from pytox import options class CoreTest(unittest.TestCase): @@ -8,25 +10,25 @@ def test_version(self): self.assertEqual(len(core.VERSION.split(".")), 3) def test_options(self): - opts = core.ToxOptions() + opts = options.ToxOptions() self.assertTrue(opts.ipv6_enabled) opts.ipv6_enabled = False self.assertFalse(opts.ipv6_enabled) def test_use_after_free(self): - opts = core.ToxOptions() - with self.assertRaises(core.UseAfterFreeException): + opts = options.ToxOptions() + with self.assertRaises(error.UseAfterFreeException): with core.Core(opts) as tox: saved_tox = tox print(saved_tox.address) def test_address(self): - opts = core.ToxOptions() + opts = options.ToxOptions() with core.Core(opts) as tox: self.assertEqual(tox.address, tox.address) def test_public_key_is_address_prefix(self): - opts = core.ToxOptions() + opts = options.ToxOptions() with core.Core(opts) as tox: self.assertEqual( tox.public_key.hex()[:72] + format(tox.nospam, "08x"), @@ -34,7 +36,7 @@ def test_public_key_is_address_prefix(self): ) def test_public_key_is_not_secret_key(self): - opts = core.ToxOptions() + opts = options.ToxOptions() with core.Core(opts) as tox: self.assertNotEqual(tox.public_key, tox.secret_key) @@ -49,7 +51,7 @@ def test_set_name(self): self.assertEqual(tox.name, b"iphy") tox.name = b"x" * core.MAX_NAME_LENGTH - with self.assertRaises(core.ApiException): + with self.assertRaises(error.ApiException): tox.name = b"x" * (core.MAX_NAME_LENGTH + 1) def test_set_status_message(self): @@ -59,7 +61,7 @@ def test_set_status_message(self): self.assertEqual(tox.status_message, b"pytox is cool now") tox.status_message = b"x" * core.MAX_STATUS_MESSAGE_LENGTH - with self.assertRaises(core.ApiException): + with self.assertRaises(error.ApiException): tox.status_message = b"x" * (core.MAX_STATUS_MESSAGE_LENGTH + 1) @@ -76,9 +78,9 @@ def test_friend_add(self): with core.Core() as tox2: tox1.friend_add(tox2.address, b"hello there!") tox2.friend_add_norequest(tox1.public_key) - with self.assertRaises(core.LengthException): + with self.assertRaises(error.LengthException): tox2.friend_add_norequest(tox1.address) - with self.assertRaises(core.LengthException): + with self.assertRaises(error.LengthException): tox2.friend_add(tox1.public_key, b"oh no!") def test_friend_delete(self): @@ -86,7 +88,7 @@ def test_friend_delete(self): with core.Core() as tox2: tox1.friend_add(tox2.address, b"hello there!") tox1.friend_delete(0) - with self.assertRaises(core.ApiException): + with self.assertRaises(error.ApiException): # Deleting again: we don't have that friend anymore. tox1.friend_delete(0) diff --git a/test/tox_options_test.py b/test/tox_options_test.py index 6659b15..8a9dcd5 100644 --- a/test/tox_options_test.py +++ b/test/tox_options_test.py @@ -1,19 +1,21 @@ import unittest from pytox import core +from pytox import error +from pytox import options class ToxOptionsTest(unittest.TestCase): def test_options(self): - opts = core.ToxOptions() + opts = options.ToxOptions() self.assertTrue(opts.ipv6_enabled) opts.ipv6_enabled = False self.assertFalse(opts.ipv6_enabled) def test_use_after_free(self): - with core.ToxOptions() as opts: + with options.ToxOptions() as opts: saved_opts = opts - with self.assertRaises(core.UseAfterFreeException): + with self.assertRaises(error.UseAfterFreeException): print(saved_opts.ipv6_enabled) diff --git a/tools/gen_api.py b/tools/gen_api.py index d3f5f83..690faaa 100755 --- a/tools/gen_api.py +++ b/tools/gen_api.py @@ -76,6 +76,7 @@ def parse_params(tokens: Tuple[str, ...]) -> List[Tuple[List[str], str]]: def finalize_handler(type_prefix: str, handlers: List[str], event: str, params: List[str]) -> None: + handlers[-1] += " noexcept" handlers[-1] += ":" self = "" array = "" @@ -114,6 +115,12 @@ def handle_ignore(tokens: Sequence[str], state: List[str]) -> bool: return False +def handle_nullable(tokens: Sequence[str], state: List[str]) -> bool: + if tokens[0] in ("non_null", "nullable"): + return True # skip the entire line + return False + + def handle_macro(tokens: Sequence[str], state: List[str]) -> bool: if tokens[0] == "#define": if tokens[-1] == "\\": @@ -141,11 +148,15 @@ def handle_types(tokens: Sequence[str], state: List[str], extern: List[str], # enums if (tokens[:2] == ("typedef", "enum") or tokens[0] == "enum") and tokens[-1] == "{": - extern.append("") - extern.append(f" cpdef enum {tokens[-2]}:") + enum_name = tokens[-2] + if enum_name != "Tox_Log_Level": + extern.append("") + extern.append(f" cpdef enum {enum_name}:") state.append("enum") elif state[-1] == "enum" and tokens[0].startswith(const_prefix): - extern.append(f" {tokens[0]},") + enumerator_name = tokens[0] + if not enumerator_name.startswith("TOX_LOG_LEVEL"): + extern.append(f" {enumerator_name},") return False @@ -207,7 +218,8 @@ def handle_functions( return event -def gen_cython(lines: Sequence[str], fun_prefix: str) -> List[str]: +def gen_cython(lines: Sequence[str], fun_prefix: str, + extern_line: str) -> List[str]: const_prefix = fun_prefix.upper() type_prefix = fun_prefix.capitalize() @@ -215,11 +227,11 @@ def gen_cython(lines: Sequence[str], fun_prefix: str) -> List[str]: extern = [] handlers: List[str] = [] - install_handlers = ["cdef void install_handlers(Tox *ptr):"] + install_handlers: List[str] = [] params: List[str] = [] event = "" - extern.append('cdef extern from "tox/tox.h":') + extern.append(extern_line) for line in lines: line = line.strip() tokens = tokenize(line) @@ -233,6 +245,9 @@ def gen_cython(lines: Sequence[str], fun_prefix: str) -> List[str]: if handle_ignore(tokens, state): continue + if handle_nullable(tokens, state): + continue + if handle_macro(tokens, state): continue @@ -263,22 +278,37 @@ def gen_cython(lines: Sequence[str], fun_prefix: str) -> List[str]: install_handlers, ) + if install_handlers: + install_handlers = ["cdef void install_handlers(Tox *ptr):" + ] + install_handlers return extern + [""] + handlers + [""] + install_handlers +def get_fun_prefix(api: str) -> str: + return os.path.split(api)[-1].split(".")[0].split("_")[0] + "_" + + def main() -> None: src = sys.argv[1] - api = sys.argv[2] + api_base = sys.argv[2] + + cdef_extern_prefix = 'cdef extern from "' + cdef_extern_suffix = '": pass\n' with open(src, "r", encoding="utf-8") as src_fh: for line in src_fh.readlines(): - if line.startswith("cdef extern from"): + if line.startswith(cdef_extern_prefix) and line.endswith( + cdef_extern_suffix): + api_file = line.removeprefix(cdef_extern_prefix).removesuffix( + cdef_extern_suffix) + api = os.path.join(api_base, api_file) + extern_line = line.removesuffix(" pass\n") with open(api, "r", encoding="utf-8") as api_fh: print("\n".join( gen_cython( api_fh.readlines(), - fun_prefix=os.path.split(api)[-1].split(".")[0] + - "_", + fun_prefix=get_fun_prefix(api), + extern_line=extern_line, ))) else: print(line.rstrip())