├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── CREDITS ├── ChangeLog ├── LICENSE ├── MANIFEST.in ├── README.rst ├── build_inplace ├── dev-requirements.txt ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── why.rst ├── fibers ├── __init__.py └── _pyfibers.py ├── setup.cfg ├── setup.py ├── src ├── fibers.c ├── fibers.h ├── slp_platformselect.h ├── stacklet.c ├── stacklet.h ├── switch_aarch64_gcc.h ├── switch_arm_gcc.h ├── switch_mips64_gcc.h ├── switch_ppc64_gcc.h ├── switch_s390x_gcc.h ├── switch_x64_msvc.asm ├── switch_x64_msvc.h ├── switch_x86_64_gcc.h ├── switch_x86_gcc.h ├── switch_x86_msvc.asm └── switch_x86_msvc.h └── tests ├── test_fibers.py ├── test_gc.py ├── test_generator.py ├── test_generator_nested.py ├── test_leaks.py ├── test_throw.py ├── test_weakref.py └── verify-version.py /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # ref: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ 2 | 3 | name: CI/CD 4 | 5 | on: 6 | push: 7 | branches: [ "*" ] 8 | tags: [ "fibers-*" ] 9 | pull_request: 10 | branches: [ "*" ] 11 | tags: [ "fibers-*" ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # would use windows-latest, but distutils tries to build against the version of MSVC that python was compiled with 21 | # https://github.com/pypa/setuptools/blob/main/setuptools/_distutils/msvc9compiler.py#L403 22 | os: [ "ubuntu-latest", "macos-12", "windows-2019" ] 23 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 24 | #os: [ "ubuntu-latest" ] 25 | #python-version: ["3.8"] 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v3 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | python -m pip install --user -r dev-requirements.txt 37 | - name: Build Packages 38 | run: | 39 | python -m build 40 | #- name: Lint with flake8 41 | # run: | 42 | # # stop the build if there are Python syntax errors or undefined names 43 | # flake8 fibers/ --count --select=E9,F63,F7,F82 --show-source --statistics 44 | # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 45 | # flake8 fibers --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 46 | - name: Install Package 47 | working-directory: ./dist 48 | run: | 49 | # this silly wildcard expansion is because powershell doesn't do it inherently but this syntax works for both bash and powershell 50 | python -m pip install -v $(ls *.whl) 51 | - name: Verify Version Number 52 | ## be sure NOT to be in the src root, as we will pick up the fibers/ folder as a module instead of the whl we installed 53 | working-directory: ./tests 54 | if: startsWith(github.ref, 'refs/tags/fibers-') 55 | run: | 56 | python verify-version.py '${{ github.ref_name }}' 57 | - name: Test with pytest 58 | ## be sure NOT to be in the src root, as we will pick up the fibers/ folder as a module instead of the whl we installed 59 | working-directory: ./tests 60 | run: | 61 | python -m pytest -v . 62 | - name: Store the distribution packages 63 | # Note: We *don't* publish wheels for linux because pypi only accepts 'manylinux' arch and GH actions don't seem to make that too easy. 64 | # So we'll just let it install from src pkg and build, which is a common practice for linux packages, being that a compiler so often installed. 65 | if: runner.os != 'Linux' && startsWith(github.ref, 'refs/tags/fibers-') 66 | uses: actions/upload-artifact@v3 67 | with: 68 | name: python-package-distributions 69 | path: dist/ 70 | 71 | publish-to-pypi: 72 | name: Publish to PyPI (if tag) 73 | if: startsWith(github.ref, 'refs/tags/fibers-') # only publish to PyPI on tag pushes 74 | needs: 75 | - build 76 | runs-on: ubuntu-latest 77 | environment: 78 | name: pypi 79 | url: https://pypi.org/p/fibers 80 | permissions: 81 | id-token: write # IMPORTANT: mandatory for trusted publishing 82 | steps: 83 | - name: Download all the dists 84 | uses: actions/download-artifact@v4.1.7 85 | with: 86 | name: python-package-distributions 87 | path: dist/ 88 | - name: Publish distribution packages to PyPI 89 | uses: pypa/gh-action-pypi-publish@release/v1 90 | 91 | github-release: 92 | name: Create GitHub Release (if tag) 93 | needs: 94 | - publish-to-pypi 95 | runs-on: ubuntu-latest 96 | 97 | permissions: 98 | contents: write # IMPORTANT: mandatory for making GitHub Releases 99 | id-token: write # IMPORTANT: mandatory for sigstore 100 | 101 | steps: 102 | - uses: actions/checkout@v3 103 | - name: Download all the dists 104 | uses: actions/download-artifact@v4.1.7 105 | with: 106 | name: python-package-distributions 107 | path: dist/ 108 | - name: Sign the dists with Sigstore 109 | uses: sigstore/gh-action-sigstore-python@v1.2.3 110 | with: 111 | inputs: >- 112 | ./dist/*.tar.gz 113 | ./dist/*.whl 114 | - name: Create GitHub Release 115 | env: 116 | GH_TOKEN: ${{ github.token }} 117 | run: >- 118 | gh release create 119 | '${{ github.ref_name }}' 120 | --notes "" 121 | - name: Upload artifact signatures to GitHub Release 122 | env: 123 | GH_TOKEN: ${{ github.token }} 124 | # Upload to GitHub Release using the `gh` CLI. 125 | # `dist/` contains the built packages, and the 126 | # sigstore-produced signatures and certificates. 127 | run: >- 128 | gh release upload 129 | '${{ github.ref_name }}' dist/** 130 | --repo '${{ github.repository }}' 131 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #python specific 2 | *.pyc 3 | *.so 4 | *.pyd 5 | build/* 6 | dist/* 7 | MANIFEST 8 | __pycache__/ 9 | *.egg-info/ 10 | 11 | ## generic files to ignore 12 | *~ 13 | *.lock 14 | *.DS_Store 15 | *.swp 16 | *.out 17 | 18 | .venv* 19 | .tox/ 20 | .gdb_history 21 | deps/ 22 | docs/_build/ 23 | 24 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | This project would not have been possible without the previous work from 2 | greenlet (http://greenlet.readthedocs.org) and stacklet / pypy (http://pypy.org) 3 | 4 | 5 | Contributors 6 | ------------ 7 | https://github.com/saghul/python-fibers/graphs/contributors 8 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | DISCONTINUED: Please see the commit history. 2 | 3 | 1.1.0 4 | ===== 5 | - Refactor Windows build 6 | - Refactor Travis builds 7 | - Only install what's needed to run tests on AppVeyor 8 | - Refactor AppVeyor builds 9 | - Removed tox file 10 | - Update README 11 | - Update stacklet 12 | 13 | 1.0.0 14 | ===== 15 | - Start keeping a changelog (check git for previous history) 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT license 2 | 3 | Copyright (C) Saúl Ibarra Corretgé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | 24 | NOTES: 25 | 26 | - stacklet.c/h and switch_* files have been copied from the PyPy project 27 | - fibers.c/h contain pieces of code from the greenlet project 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CREDITS README.rst LICENSE 2 | include build_inplace setup.py setup.cfg tox.ini 3 | recursive-include docs * 4 | recursive-include src * 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | prune docs/_build 9 | 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | fibers: lightweight concurrent multitasking 3 | =========================================== 4 | 5 | .. image:: https://badge.fury.io/py/fibers.png 6 | :target: http://badge.fury.io/py/fibers 7 | 8 | Overview 9 | ======== 10 | 11 | Fibers are lightweight primitives for cooperative multitasking in Python. They 12 | provide means for running pieces of code that can be paused and resumed. Unlike 13 | threads, which are preemptively scheduled, fibers are scheduled cooperatively, 14 | that is, only one fiber will be running at a given point in time, and no other 15 | fiber will run until the user explicitly decides so. 16 | 17 | When a fiber is created it will not run automatically. A fiber must be 'switched' 18 | into for it to run. Fibers can switch control to other fibers by way of the `switch` 19 | or `throw` functions, which switch control or raise and exception in the target 20 | fiber respectively. 21 | 22 | Example: 23 | 24 | :: 25 | 26 | import fibers 27 | 28 | def func1(): 29 | print "1" 30 | f2.switch() 31 | print "3" 32 | f2.switch() 33 | 34 | def func2(): 35 | print "2" 36 | f1.switch() 37 | print "4" 38 | 39 | f1 = fibers.Fiber(target=func1) 40 | f2 = fibers.Fiber(target=func2) 41 | f1.switch() 42 | 43 | 44 | The above example will print "1 2 3 4", but the result was obtained by the 45 | cooperative work of 2 fibers yielding control to each other. 46 | 47 | 48 | CI status 49 | ========= 50 | 51 | .. image:: https://github.com/saghul/python-fibers/actions/workflows/python-package.yml/badge.svg 52 | ::target: https://github.com/saghul/python-fibers/actions/ 53 | 54 | 55 | Documentation 56 | ============= 57 | 58 | http://readthedocs.org/docs/python-fibers/ 59 | 60 | 61 | Installing 62 | ========== 63 | 64 | fibers can be installed via pip as follows: 65 | 66 | :: 67 | 68 | pip install fibers 69 | 70 | 71 | Building 72 | ======== 73 | 74 | Get the source: 75 | 76 | :: 77 | 78 | git clone https://github.com/saghul/python-fibers 79 | 80 | 81 | Linux: 82 | 83 | :: 84 | 85 | ./build_inplace 86 | 87 | Mac OSX: 88 | 89 | :: 90 | 91 | (XCode needs to be installed) 92 | export ARCHFLAGS="-arch x86_64" 93 | ./build_inplace 94 | 95 | Microsoft Windows: 96 | 97 | :: 98 | 99 | python setup.py build_ext --inplace 100 | 101 | 102 | Running the test suite 103 | ====================== 104 | 105 | The test suite can be run using pytest: 106 | 107 | :: 108 | 109 | python -m pytest -v . 110 | 111 | 112 | Author 113 | ====== 114 | 115 | Saúl Ibarra Corretgé 116 | 117 | This project would not have been possible without the previous work done in 118 | the `greenlet `_ and stacklet (part of 119 | `PyPy `_) projects. 120 | 121 | 122 | License 123 | ======= 124 | 125 | Unless stated otherwise on-file fibers uses the MIT license, check LICENSE file. 126 | 127 | 128 | Supported Python versions 129 | ========================= 130 | 131 | Python >= 3.7 are supported. Other older Python versions might work, but 132 | they are not actively tested. CPython and PyPy are supported. 133 | 134 | 135 | Supported architectures 136 | ======================= 137 | 138 | x86, x86-64, ARM, ARM64, MIPS64, PPC64 and s390x are supported. 139 | 140 | 141 | Contributing 142 | ============ 143 | 144 | If you'd like to contribute, fork the project, make a patch and send a pull 145 | request. Have a look at the surrounding code and please, make yours look 146 | alike. 147 | 148 | -------------------------------------------------------------------------------- /build_inplace: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python setup.py build_ext --inplace $@ 4 | 5 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | build 2 | pytest 3 | sphinx 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/fibers.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/fibers.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/fibers" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/fibers" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # fibers documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jul 25 02:22:04 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import re 15 | 16 | def get_version(): 17 | return re.search(r"""__version__\s+=\s+(?P['"])(?P.+?)(?P=quote)""", open('../fibers/__init__.py').read()).group('version') 18 | _version = get_version() 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = [] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'fibers' 48 | copyright = u'2013, Saúl Ibarra Corretgé' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = _version 56 | # The full version, including alpha/beta/rc tags. 57 | release = _version 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = ['_build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | html_theme = 'nature' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | #html_theme_path = [] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'fibersdoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | #'papersize': 'letterpaper', 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #'pointsize': '10pt', 182 | 183 | # Additional stuff for the LaTeX preamble. 184 | #'preamble': '', 185 | } 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'fibers.tex', u'fibers Documentation', 191 | u'Saúl Ibarra Corretgé', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | #latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | #latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'fibers', u'fibers Documentation', 221 | [u'Saúl Ibarra Corretgé'], 1) 222 | ] 223 | 224 | # If true, show URL addresses after external links. 225 | #man_show_urls = False 226 | 227 | 228 | # -- Options for Texinfo output ------------------------------------------------ 229 | 230 | # Grouping the document tree into Texinfo files. List of tuples 231 | # (source start file, target name, title, author, 232 | # dir menu entry, description, category) 233 | texinfo_documents = [ 234 | ('index', 'fibers', u'fibers Documentation', 235 | u'Saúl Ibarra Corretgé', 'fibers', 'One line description of project.', 236 | 'Miscellaneous'), 237 | ] 238 | 239 | # Documents to append as an appendix to all manuals. 240 | #texinfo_appendices = [] 241 | 242 | # If false, no module index is generated. 243 | #texinfo_domain_indices = True 244 | 245 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 246 | #texinfo_show_urls = 'footnote' 247 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | fibers: lightweight concurrent multitasking 3 | =========================================== 4 | 5 | .. module:: fibers 6 | 7 | Overview 8 | -------- 9 | 10 | Fibers are lightweight primitives for cooperative multitasking in Python. They 11 | provide means for running pieces of code that can be paused and resumed. Unlike 12 | threads, which are preemptively scheduled, fibers are scheduled cooperatively, 13 | that is, only one fiber will be running at a given point in time, and no other 14 | fiber will run until the user explicitly decides so. 15 | 16 | When a fiber is created it will not run automatically. A fiber must be 'switched' 17 | into for it to run. Fibers can switch control to other fibers by way of the `switch` 18 | or `throw` functions, which switch control or raise and exception in the target 19 | fiber respectively. 20 | 21 | 22 | Motivation 23 | ---------- 24 | 25 | I started this project mainly because I wanted a slightly different API from 26 | what greenlet offers. After playing with the code a bit I decided to make more 27 | changes and *fibers* became what it is today. 28 | 29 | "Why not just fork greenlet and make the changes?" you probably wonder. Since 30 | I wanted to make some fundamental changes to the API changes were going to be big 31 | and since I was there I thought I may as well use the stack switching implementation 32 | used by PyPy, which has some advantages over the current one used in greenlet. 33 | 34 | For the curious, I made a list of differences between fibers and greenlet here, 35 | see :ref:`why-fibers`. 36 | 37 | 38 | API 39 | --- 40 | 41 | The ``fibers`` module exports two objects, the ``Fiber`` type and the ``error`` object. 42 | 43 | .. py:class:: Fiber([target, [args, [kwargs, [parent]]]]) 44 | 45 | :param callable target: callable which this fiber will execute when switched to. 46 | 47 | :param tuple args: tuple of arguments which the specified target callable will 48 | get passed. 49 | 50 | :param dict kwargs: dictionary of keyword arguments the specified callable 51 | will get passed. 52 | 53 | :type parent: :py:class:`Fiber` 54 | :param parent: parent fiber for this object. If not specified, the current one 55 | will be used. 56 | 57 | ``Fiber`` objects are lightweight microthreads which are cooperatively scheduled. 58 | Only one can run at a given time and the ``switch`` and/or ``throw`` functions 59 | must be used to switch execution from one fiber to another. 60 | 61 | The first time a fiber is switched into, the specified callable will be called 62 | with the given arguments and keyword arguments. 63 | 64 | :: 65 | 66 | def runner(*args, **kwargs): 67 | return 42 68 | 69 | f = Fiber(target=runner, args=(1, 2, 3), kwargs={'foo': 123} 70 | f.switch() 71 | 72 | When ``f.switch()`` is called the ``runner`` function will be executed, with the 73 | given positional and keyword arguments. 74 | 75 | 76 | .. py:method:: switch([value]) 77 | 78 | :param object value: Arbitrary object which will be returned to the fiber 79 | which called ``switch`` on this fiber before. A value can only specified 80 | if the fiber has already been started. In order to start a fiber, this 81 | function must be called without a value. 82 | 83 | Suspend the current running fiber and switch execution to the target fiber. 84 | If a value is specified, the fiber which previously called ``switch()`` will 85 | appear to return the value specified here. 86 | 87 | .. py:method:: throw(typ, [val, [tb]]) 88 | 89 | :param type typ: Exception type to be raised. 90 | 91 | :param object val: Exception instance value to be raised. 92 | 93 | :param traceback tb: Traceback object. 94 | 95 | Suspend the current running fiber and switch execution to the target fiber, 96 | raising the specified exception immediately. The fiber which is resumed will 97 | get the exception raised, and if it's not caught it will be propagated to 98 | the parent. 99 | 100 | .. py:method:: is_alive 101 | 102 | Returns `True` if the fiber hasn't ended yet, `False` if it has already ended. 103 | 104 | .. py:classmethod:: current 105 | 106 | Returns the current ``Fiber`` object. 107 | 108 | 109 | .. py:exception:: error 110 | 111 | Exception raised by this module when an error such as trying to switch to a fiber 112 | in a different thread occurs. 113 | 114 | 115 | .. py:function:: current 116 | 117 | Returns the current ``Fiber`` object. 118 | 119 | 120 | Parents 121 | ------- 122 | 123 | Fibers are organized in a tree form. Each native Python thread has a fibers tree, 124 | which is initialized the first time a fiber is created. When a fiber is created 125 | the user can select what the parent fiber will be. When that fiber finishes 126 | execution, control will be switched to the parent. 127 | 128 | 129 | Multi-threading 130 | --------------- 131 | 132 | There is no multithreading support, that is, a fiber in one thread cannot switch 133 | control to a fiber in a different thread, this will raise an exception. Likewise, 134 | a fiber cannot get assigned a parent which belongs to a different thread. 135 | 136 | Note: a fiber is bound to the thread where it was created, and this cannot be 137 | changed. 138 | 139 | 140 | 141 | Indices and tables 142 | ================== 143 | 144 | * :ref:`genindex` 145 | * :ref:`modindex` 146 | * :ref:`search` 147 | 148 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\fibers.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\fibers.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/why.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _why-fibers: 3 | 4 | Why fibers 5 | ========== 6 | 7 | Fibers was created because I wanted to implement an API which looked pretty much 8 | like a thread but with greenlet, but the API offered by greenlet got in the way, 9 | so I decided to try and extract the minimum amount of functionality I needed and 10 | make the API *I* wanted to use. 11 | 12 | Note the use of the first person singular here, this was created for myself, I'm 13 | sharing it because it may also help others. If this helped you, let me know, I'd 14 | like to hear your use case! 15 | 16 | 17 | Early binding 18 | ------------- 19 | 20 | This was the first thing I wanted to change. In greenlet, the target function can 21 | be specified in ``__init__`` but it can be changed later (before the greenlet is 22 | switched to for the first time) and arguments (and keyword arguments) must be passed 23 | to ``switch``. I wanted to 'bind' the callable and arguments as early as possible 24 | (in ``__init__``, actually) pretty much like threads do. 25 | 26 | With greenlet 27 | 28 | :: 29 | 30 | def run(*args, **kwargs): 31 | print args 32 | print kwargs 33 | 34 | g = greenlet(run) 35 | g.switch(1, 2, 3, foo='bar') 36 | 37 | With fibers 38 | 39 | :: 40 | 41 | def run(*args, **kwargs): 42 | print args 43 | print kwargs 44 | 45 | f = Fiber(target=run, args=(1, 2, 3), kwargs={'foo':'bar'}) 46 | f.switch() 47 | 48 | 49 | This is a bigger change than it seems, because it also means that ``switch`` only 50 | gets one argument, which is what the called will get. In greenlet the caller may 51 | get a tuple, a dictionary or a tuple containing another tuple and a dictionary, 52 | depending on the values passed to ``switch``. 53 | 54 | 55 | Graceful failures 56 | ----------------- 57 | 58 | Greenlet tends to be quite graceful with failures, which may lead to unexpected 59 | behavior. 60 | 61 | With greenlet 62 | 63 | :: 64 | 65 | def run(): 66 | return 42 67 | 68 | g = greenlet(run) 69 | res = g.switch() 70 | # res is 42 71 | res = g.switch() 72 | # res is the empty tuple 73 | 74 | With fibers 75 | 76 | :: 77 | 78 | def run(): 79 | return 42 80 | 81 | f = Fiber(run) 82 | res = f.switch() 83 | # res is 42 84 | res = f.switch() 85 | # raises fiibers.error exception because f has ended 86 | 87 | 88 | Garbage collection magic 89 | ------------------------ 90 | 91 | This is a tricky one. When greenlets are garbage collected, they are switched into 92 | (if they where running) and ``GreenletExit`` exception is raised in them. This means 93 | that if the code caught this exception, it could resurrect the object. 94 | 95 | In fibers, on the contrary, this doesn't happen and if a fiber is gc'd while alive, 96 | it's not switched into and no exception is raised in it. 97 | 98 | This means that the programmer needs to take care and ``throw`` into the fibers which 99 | he/she wishes to kill. It may look like a burden at a first sight, but I believe 100 | it's for the best. 101 | 102 | 103 | Stack slicing implementation 104 | ---------------------------- 105 | 106 | I'll be honest: I'm not smart enough to write the assembly code required to 107 | perform the actual stack slicing for the different platforms. That's why I stand 108 | on the shoulders of giants, in this case, I leveraged the tiny `stacklet` library 109 | hidden inside PyPy, which is used to implement continuations, as well as 110 | the greenlet module (in PyPy, that is). 111 | 112 | `Stacklet` implements save, restore and stack switch operations in assembly, which 113 | compilers won't touch, so there should be no issues regardless of the optimizations 114 | used and the 'smartness' of the compiler. The greenlet implementation, on the other 115 | hand, only implements the stack switch operation in assembly, and workarounds have 116 | been needed every now and then, as compilers have become 'smarter'. 117 | 118 | On a personal note, using a library such as `stacklet` makes the code look simpler, 119 | since fibers is a wrapper over it with a cherry on top, and all the scary bits are 120 | hidden inside the library itself. 121 | 122 | -------------------------------------------------------------------------------- /fibers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '1.2.0' 3 | 4 | try: 5 | from fibers._cfibers import * 6 | except ImportError: 7 | from fibers._pyfibers import * 8 | 9 | -------------------------------------------------------------------------------- /fibers/_pyfibers.py: -------------------------------------------------------------------------------- 1 | 2 | import _continuation 3 | import threading 4 | 5 | __all__ = ['Fiber', 'error', 'current'] 6 | 7 | 8 | _tls = threading.local() 9 | 10 | 11 | def current(): 12 | try: 13 | return _tls.current_fiber 14 | except AttributeError: 15 | fiber = _tls.current_fiber = _tls.main_fiber = _create_main_fiber() 16 | return fiber 17 | 18 | 19 | class error(Exception): 20 | pass 21 | 22 | 23 | class Fiber(object): 24 | _cont = None 25 | _thread_id = None 26 | _ended = False 27 | 28 | def __init__(self, target=None, args=[], kwargs={}, parent=None): 29 | def _run(c): 30 | _tls.current_fiber = self 31 | try: 32 | return target(*args, **kwargs) 33 | finally: 34 | cont = self._cont 35 | self._cont = None 36 | self._ended = True 37 | _continuation.permute(cont, self._get_active_parent()._cont) 38 | 39 | self._func = _run 40 | 41 | if parent is None: 42 | parent = current() 43 | self._thread_id = threading.current_thread().ident 44 | if self._thread_id != parent._thread_id: 45 | raise error('parent cannot be on a different thread') 46 | self.parent = parent 47 | 48 | def _get_active_parent(self): 49 | parent = self.parent 50 | while True: 51 | if parent is not None and parent._cont is not None and not parent._ended: 52 | break 53 | parent = parent.parent 54 | return parent 55 | 56 | @classmethod 57 | def current(cls): 58 | return current() 59 | 60 | @property 61 | def parent(self): 62 | return self.__dict__.get('parent', None) 63 | 64 | @parent.setter 65 | def parent(self, value): 66 | if not isinstance(value, Fiber): 67 | raise TypeError('parent must be a Fiber') 68 | if value._ended: 69 | raise ValueError('parent must not have ended') 70 | if self._thread_id != value._thread_id: 71 | raise ValueError('parent cannot be on a different thread') 72 | self.__dict__['parent'] = value 73 | 74 | def switch(self, value=None): 75 | if self._ended: 76 | raise error('Fiber has ended') 77 | 78 | curr = current() 79 | if curr._thread_id != self._thread_id: 80 | raise error('Cannot switch to a fiber on a different thread') 81 | 82 | if self._cont is None: 83 | self._cont = _continuation.continulet(self._func) 84 | 85 | try: 86 | return curr._cont.switch(value=value, to=self._cont) 87 | finally: 88 | _tls.current_fiber = curr 89 | 90 | def throw(self, *args): 91 | if self._ended: 92 | raise error('Fiber has ended') 93 | 94 | curr = current() 95 | if curr._thread_id != self._thread_id: 96 | raise error('Cannot switch to a fiber on a different thread') 97 | 98 | if self._cont is None: 99 | # Fiber was not started yet, propagate to parent directly 100 | self._ended = True 101 | return self._get_active_parent().throw(*args) 102 | 103 | try: 104 | return curr._cont.throw(*args, to=self._cont) 105 | finally: 106 | _tls.current_fiber = curr 107 | 108 | def is_alive(self): 109 | return (self._cont is not None and self._cont.is_pending()) or \ 110 | (self._cont is None and not self._ended) 111 | 112 | def __getstate__(self): 113 | raise TypeError('cannot serialize Fiber object') 114 | 115 | 116 | def _create_main_fiber(): 117 | main_fiber = Fiber.__new__(Fiber) 118 | main_fiber._cont = _continuation.continulet.__new__(_continuation.continulet) 119 | main_fiber._ended = False 120 | main_fiber._thread_id = threading.current_thread().ident 121 | main_fiber.__dict__['parent'] = None 122 | return main_fiber 123 | 124 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = -v 3 | testpaths = 4 | tests 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from setuptools import setup, Extension 4 | 5 | import glob 6 | import os 7 | import re 8 | import sys 9 | 10 | 11 | if hasattr(sys, 'pypy_version_info'): 12 | ext_modules = [] 13 | else: 14 | extra_objects = [] 15 | if sys.platform == 'win32': 16 | # This is a hack because msvc9compiler doesn't support asm files 17 | # http://bugs.python.org/issue7546 18 | 19 | # Initialize compiler 20 | from distutils.msvc9compiler import MSVCCompiler 21 | cc = MSVCCompiler() 22 | cc.initialize() 23 | del cc 24 | 25 | if '32 bit' in sys.version: 26 | extra_objects = ['src/switch_x86_msvc.obj'] 27 | os.system('ml /nologo /c /Fo src\\switch_x86_msvc.obj src\\switch_x86_msvc.asm') 28 | else: 29 | extra_objects = ['src/switch_x64_msvc.obj'] 30 | os.system('ml64 /nologo /c /Fo src\\switch_x64_msvc.obj src\\switch_x64_msvc.asm') 31 | 32 | ext_modules = [Extension('fibers._cfibers', 33 | sources=glob.glob('src/*.c'), 34 | extra_objects=extra_objects, 35 | )] 36 | 37 | 38 | def get_version(): 39 | return re.search(r"""__version__\s+=\s+(?P['"])(?P.+?)(?P=quote)""", open('fibers/__init__.py').read()).group('version') 40 | 41 | 42 | setup(name = 'fibers', 43 | version = get_version(), 44 | author = 'Saúl Ibarra Corretgé', 45 | author_email = 's@saghul.net', 46 | url = 'http://github.com/saghul/python-fibers', 47 | description = 'Lightweight cooperative microthreads for Pyhton', 48 | long_description = open('README.rst').read(), 49 | packages = ['fibers'], 50 | platforms = ['POSIX', 'Microsoft Windows'], 51 | classifiers = [ 52 | 'Development Status :: 5 - Production/Stable', 53 | 'Intended Audience :: Developers', 54 | 'License :: OSI Approved :: MIT License', 55 | 'Operating System :: POSIX', 56 | 'Operating System :: Microsoft :: Windows', 57 | 'Programming Language :: Python', 58 | 'Programming Language :: Python :: 3', 59 | 'Programming Language :: Python :: 3.7', 60 | 'Programming Language :: Python :: 3.8', 61 | 'Programming Language :: Python :: 3.9', 62 | 'Programming Language :: Python :: 3.10', 63 | 'Programming Language :: Python :: Implementation :: CPython', 64 | 'Programming Language :: Python :: Implementation :: PyPy', 65 | ], 66 | ext_modules = ext_modules 67 | ) 68 | -------------------------------------------------------------------------------- /src/fibers.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include "fibers.h" 4 | 5 | typedef struct { 6 | Fiber *origin; 7 | PyObject *value; 8 | } FiberGlobalState; 9 | 10 | static volatile FiberGlobalState _global_state; 11 | 12 | static PyObject* main_fiber_key; 13 | static PyObject* current_fiber_key; 14 | 15 | static PyObject* PyExc_FiberError; 16 | 17 | 18 | /* 19 | * Create main Fiber. There is always a main Fiber for a given (real) thread, 20 | * and it's parent is always NULL. 21 | */ 22 | static Fiber * 23 | fiber_create_main(void) 24 | { 25 | Fiber *t_main; 26 | PyObject *dict = PyThreadState_GetDict(); 27 | PyTypeObject *cls = (PyTypeObject *)&FiberType; 28 | 29 | ASSERT(dict != NULL); 30 | 31 | /* create the main Fiber for this thread */ 32 | t_main = (Fiber *)cls->tp_new(cls, NULL, NULL); 33 | if (!t_main) { 34 | return NULL; 35 | } 36 | Py_INCREF(dict); 37 | t_main->ts_dict = dict; 38 | t_main->parent = NULL; 39 | t_main->thread_h = stacklet_newthread(); 40 | t_main->stacklet_h = NULL; 41 | t_main->initialized = True; 42 | t_main->is_main = True; 43 | return t_main; 44 | } 45 | 46 | 47 | /* 48 | * Get the current Fiber reference on the current thread. The first time this 49 | * function is called on a given (real) thread, the main Fiber is created. 50 | */ 51 | static Fiber * 52 | get_current(void) 53 | { 54 | Fiber *current; 55 | PyObject *tstate_dict; 56 | 57 | /* get current Fiber from the active thread-state */ 58 | tstate_dict = PyThreadState_GetDict(); 59 | if (tstate_dict == NULL) { 60 | if (!PyErr_Occurred()) { 61 | PyErr_NoMemory(); 62 | } 63 | return NULL; 64 | } 65 | current = (Fiber *)PyDict_GetItem(tstate_dict, current_fiber_key); 66 | if (current == NULL) { 67 | current = fiber_create_main(); 68 | if (current == NULL) { 69 | return NULL; 70 | } 71 | /* Keep a reference to the main fiber in the thread dict. The main 72 | * fiber is special because we don't require the user to keep a 73 | * reference to it. It should be deleted when the thread exits. */ 74 | if (PyDict_SetItem(tstate_dict, main_fiber_key, (PyObject *) current) < 0) { 75 | Py_DECREF(current); 76 | return NULL; 77 | } 78 | /* current starts out as main */ 79 | if (PyDict_SetItem(tstate_dict, current_fiber_key, (PyObject *) current) < 0) { 80 | Py_DECREF(current); 81 | return NULL; 82 | } 83 | /* return a borrowed ref. refcount should be 2 after this */ 84 | Py_DECREF(current); 85 | } 86 | 87 | ASSERT(current != NULL); 88 | return current; 89 | } 90 | 91 | 92 | /* 93 | * Get current Fiber 94 | */ 95 | static PyObject * 96 | fibers_func_current(PyObject *obj) 97 | { 98 | Fiber *current; 99 | 100 | UNUSED_ARG(obj); 101 | 102 | if (!(current = get_current())) { 103 | return NULL; 104 | } 105 | Py_INCREF(current); 106 | return (PyObject *) current; 107 | } 108 | 109 | 110 | static int 111 | Fiber_tp_init(Fiber *self, PyObject *args, PyObject *kwargs) 112 | { 113 | static char *kwlist[] = {"target", "args", "kwargs", "parent", NULL}; 114 | 115 | PyObject *target, *t_args, *t_kwargs; 116 | Fiber *current, *parent; 117 | target = t_args = t_kwargs = NULL; 118 | parent = NULL; 119 | 120 | if (self->initialized) { 121 | PyErr_SetString(PyExc_RuntimeError, "object was already initialized"); 122 | return -1; 123 | } 124 | 125 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OOOO!:__init__", kwlist, &target, &t_args, &t_kwargs, &FiberType, &parent)) { 126 | return -1; 127 | } 128 | 129 | if (!(current = get_current())) { 130 | return -1; 131 | } 132 | 133 | if (parent) { 134 | /* check if parent is on the same (real) thread */ 135 | if (parent->ts_dict != current->ts_dict) { 136 | PyErr_SetString(PyExc_FiberError, "parent cannot be on a different thread"); 137 | return -1; 138 | } 139 | if (parent->stacklet_h == EMPTY_STACKLET_HANDLE) { 140 | PyErr_SetString(PyExc_ValueError, "parent must not have ended"); 141 | return -1; 142 | } 143 | } else { 144 | parent = current; 145 | } 146 | 147 | if (target) { 148 | if (!PyCallable_Check(target)) { 149 | PyErr_SetString(PyExc_TypeError, "if specified, target must be a callable"); 150 | return -1; 151 | } 152 | if (t_args) { 153 | if (!PyTuple_Check(t_args)) { 154 | PyErr_SetString(PyExc_TypeError, "args must be a tuple"); 155 | return -1; 156 | } 157 | } else { 158 | t_args = PyTuple_New(0); 159 | } 160 | if (t_kwargs) { 161 | if (!PyDict_Check(t_kwargs)) { 162 | PyErr_SetString(PyExc_TypeError, "kwargs must be a dict"); 163 | return -1; 164 | } 165 | } 166 | } 167 | 168 | Py_XINCREF(target); 169 | Py_XINCREF(t_args); 170 | Py_XINCREF(t_kwargs); 171 | self->target = target; 172 | self->args = t_args; 173 | self->kwargs = t_kwargs; 174 | 175 | Py_INCREF(parent); 176 | self->parent = parent; 177 | self->thread_h = parent->thread_h; 178 | self->ts_dict = parent->ts_dict; 179 | Py_INCREF(self->ts_dict); 180 | 181 | self->initialized = True; 182 | return 0; 183 | } 184 | 185 | 186 | static PyObject * 187 | Fiber_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) 188 | { 189 | Fiber *self = (Fiber *)PyType_GenericNew(type, args, kwargs); 190 | if (!self) { 191 | return NULL; 192 | } 193 | self->dict = NULL; 194 | self->ts_dict = NULL; 195 | self->weakreflist = NULL; 196 | self->parent = NULL; 197 | self->thread_h = NULL; 198 | self->stacklet_h = NULL; 199 | self->initialized = False; 200 | self->is_main = False; 201 | return (PyObject *)self; 202 | } 203 | 204 | 205 | static stacklet_handle 206 | stacklet__callback(stacklet_handle h, void *arg) 207 | { 208 | Fiber *origin, *self, *target; 209 | PyObject *result, *value; 210 | PyThreadState *tstate; 211 | stacklet_handle target_h; 212 | 213 | self = get_current(); 214 | ASSERT(self != NULL); 215 | origin = _global_state.origin; 216 | value = _global_state.value; 217 | 218 | /* save the handle to switch back to the fiber that created us */ 219 | origin->stacklet_h = h; 220 | 221 | /* set current thread state before starting this new Fiber */ 222 | tstate = PyThreadState_Get(); 223 | ASSERT(tstate != NULL); 224 | tstate->exc_state.exc_value = NULL; 225 | #if PY_MINOR_VERSION < 11 226 | tstate->frame = NULL; 227 | tstate->exc_state.exc_type = NULL; 228 | tstate->exc_state.exc_traceback = NULL; 229 | #else 230 | tstate->datastack_chunk = NULL; 231 | tstate->datastack_top = NULL; 232 | tstate->datastack_limit = NULL; 233 | tstate->cframe->current_frame = NULL; 234 | #endif 235 | tstate->exc_state.previous_item = NULL; 236 | 237 | self->ts.frame = NULL; 238 | self->ts.exc_state.exc_value = NULL; 239 | #if PY_MINOR_VERSION < 11 240 | self->ts.recursion_depth = tstate->recursion_depth; 241 | self->ts.exc_state.exc_type = NULL; 242 | self->ts.exc_state.exc_traceback = NULL; 243 | #else 244 | #if PY_MINOR_VERSION < 12 245 | self->ts.recursion_depth = tstate->recursion_limit - tstate->recursion_remaining; 246 | #else 247 | self->ts.recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; 248 | self->ts.c_recursion_remaining = tstate->c_recursion_remaining; 249 | #endif 250 | self->ts.cframe = NULL; 251 | self->ts.datastack_chunk = NULL; 252 | self->ts.datastack_top = NULL; 253 | self->ts.datastack_limit = NULL; 254 | #endif 255 | self->ts.exc_state.previous_item = NULL; 256 | 257 | if (value == NULL) { 258 | /* pending exception, user called throw on a non-started Fiber, 259 | * propagate to parent */ 260 | result = NULL; 261 | } else if (self->target) { 262 | result = PyObject_Call(self->target, self->args, self->kwargs); 263 | } else { 264 | result = Py_None; 265 | Py_INCREF(Py_None); 266 | } 267 | 268 | /* cleanup target and arguments */ 269 | Py_XDECREF(self->target); 270 | Py_XDECREF(self->args); 271 | Py_XDECREF(self->kwargs); 272 | self->target = NULL; 273 | self->args = NULL; 274 | self->kwargs = NULL; 275 | 276 | /* this Fiber has finished, select the parent as the next one to be run */ 277 | target_h = NULL; 278 | target = self->parent; 279 | while(target) { 280 | if (target->stacklet_h && target->stacklet_h != EMPTY_STACKLET_HANDLE) { 281 | _global_state.value = result; 282 | _global_state.origin = self; 283 | target_h = target->stacklet_h; 284 | break; 285 | } 286 | target = target->parent; 287 | } 288 | 289 | ASSERT(target_h); 290 | return target_h; 291 | } 292 | 293 | 294 | static PyObject * 295 | do_switch(Fiber *self, PyObject *value) 296 | { 297 | PyThreadState *tstate; 298 | stacklet_handle stacklet_h; 299 | Fiber *origin, *current; 300 | PyObject *result; 301 | 302 | /* save state */ 303 | current = get_current(); 304 | ASSERT(current != NULL); 305 | tstate = PyThreadState_Get(); 306 | ASSERT(tstate != NULL); 307 | ASSERT(tstate->dict != NULL); 308 | current->ts.exc_state.exc_value = tstate->exc_state.exc_value; 309 | #if PY_MINOR_VERSION < 11 310 | current->ts.recursion_depth = tstate->recursion_depth; 311 | current->ts.frame = tstate->frame; 312 | current->ts.exc_state.exc_type = tstate->exc_state.exc_type; 313 | current->ts.exc_state.exc_traceback = tstate->exc_state.exc_traceback; 314 | #else 315 | #if PY_MINOR_VERSION < 12 316 | current->ts.recursion_depth = tstate->recursion_limit - tstate->recursion_remaining; 317 | #else 318 | current->ts.recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; 319 | current->ts.c_recursion_remaining = tstate->c_recursion_remaining; 320 | #endif 321 | current->ts.frame = PyThreadState_GetFrame(tstate); 322 | Py_XDECREF(current->ts.frame); 323 | current->ts.cframe = tstate->cframe; 324 | current->ts.datastack_chunk = tstate->datastack_chunk; 325 | current->ts.datastack_top = tstate->datastack_top; 326 | current->ts.datastack_limit = tstate->datastack_limit; 327 | #endif 328 | current->ts.exc_state.previous_item = tstate->exc_state.previous_item; 329 | ASSERT(current->stacklet_h == NULL); 330 | 331 | /* _global_state is to pass values across a switch. Its contents are only 332 | * valid immediately before and after a switch. For any other purpose, the 333 | * current fiber is identified by current_fiber_key in the thread state 334 | * dictionary. */ 335 | _global_state.origin = current; 336 | _global_state.value = value; 337 | 338 | /* make the target fiber the new current one. */ 339 | if (PyDict_SetItem(tstate->dict, current_fiber_key, (PyObject *) self) < 0) { 340 | return NULL; 341 | } 342 | 343 | /* switch to existing, or create new fiber */ 344 | if (self->stacklet_h == NULL) { 345 | stacklet_h = stacklet_new(self->thread_h, stacklet__callback, NULL); 346 | } else { 347 | stacklet_h = stacklet_switch(self->stacklet_h); 348 | } 349 | 350 | /* need to store the handle of the stacklet that switched to us, so that 351 | * later it can be resumed again. (stacklet_h can also be 352 | * EMPTY_STACKLET_HANDLE in which case the stacklet exited) */ 353 | ASSERT(stacklet_h != NULL); 354 | origin = _global_state.origin; 355 | origin->stacklet_h = stacklet_h; 356 | current->stacklet_h = NULL; /* handle is valid only once */ 357 | result = _global_state.value; 358 | 359 | /* back to the fiber that did the switch. this may drop the refcount on 360 | * origin to zero. */ 361 | if (PyDict_SetItem(tstate->dict, current_fiber_key, (PyObject *) current) < 0) { 362 | return NULL; 363 | } 364 | 365 | /* restore state */ 366 | tstate->exc_state.exc_value = current->ts.exc_state.exc_value; 367 | #if PY_MINOR_VERSION < 11 368 | tstate->recursion_depth = current->ts.recursion_depth; 369 | tstate->frame = current->ts.frame; 370 | tstate->exc_state.exc_type = current->ts.exc_state.exc_type; 371 | tstate->exc_state.exc_traceback = current->ts.exc_state.exc_traceback; 372 | #else 373 | #if PY_MINOR_VERSION < 12 374 | tstate->recursion_remaining = tstate->recursion_limit - current->ts.recursion_depth; 375 | #else 376 | tstate->py_recursion_remaining = tstate->py_recursion_limit - current->ts.recursion_depth; 377 | tstate->c_recursion_remaining = current->ts.c_recursion_remaining; 378 | #endif 379 | tstate->cframe = current->ts.cframe; 380 | tstate->datastack_chunk = current->ts.datastack_chunk; 381 | tstate->datastack_top = current->ts.datastack_top; 382 | tstate->datastack_limit = current->ts.datastack_limit; 383 | #endif 384 | tstate->exc_state.previous_item = current->ts.exc_state.previous_item; 385 | 386 | current->ts.frame = NULL; 387 | current->ts.exc_state.exc_value = NULL; 388 | #if PY_MINOR_VERSION < 11 389 | current->ts.exc_state.exc_type = NULL; 390 | current->ts.exc_state.exc_traceback = NULL; 391 | #else 392 | current->ts.cframe = NULL; 393 | current->ts.datastack_chunk = NULL; 394 | current->ts.datastack_top = NULL; 395 | current->ts.datastack_limit = NULL; 396 | #endif 397 | current->ts.exc_state.previous_item = NULL; 398 | 399 | return result; 400 | } 401 | 402 | 403 | static PyObject * 404 | Fiber_func_switch(Fiber *self, PyObject *args) 405 | { 406 | Fiber *current; 407 | PyObject *value = Py_None; 408 | 409 | if (!PyArg_ParseTuple(args, "|O:switch", &value)) { 410 | return NULL; 411 | } 412 | 413 | if (!(current = get_current())) { 414 | return NULL; 415 | } 416 | 417 | if (self == current) { 418 | PyErr_SetString(PyExc_FiberError, "cannot switch from a Fiber to itself"); 419 | return NULL; 420 | } 421 | 422 | if (self->stacklet_h == EMPTY_STACKLET_HANDLE) { 423 | PyErr_SetString(PyExc_FiberError, "Fiber has ended"); 424 | return NULL; 425 | } 426 | 427 | if (self->thread_h != current->thread_h) { 428 | PyErr_SetString(PyExc_FiberError, "cannot switch to a Fiber on a different thread"); 429 | return NULL; 430 | } 431 | 432 | if (self->stacklet_h == NULL && value != Py_None) { 433 | PyErr_SetString(PyExc_ValueError, "cannot specify a value when the Fiber wasn't started"); 434 | return NULL; 435 | } 436 | Py_INCREF(value); 437 | 438 | return do_switch(self, value); 439 | } 440 | 441 | 442 | static PyObject * 443 | Fiber_func_throw(Fiber *self, PyObject *args) 444 | { 445 | Fiber *current; 446 | PyObject *typ, *val, *tb; 447 | 448 | val = tb = NULL; 449 | 450 | if (!PyArg_ParseTuple(args, "O|OO:throw", &typ, &val, &tb)) { 451 | return NULL; 452 | } 453 | 454 | /* First, check the traceback argument, replacing None, with NULL */ 455 | if (tb == Py_None) { 456 | tb = NULL; 457 | } else if (tb != NULL && !PyTraceBack_Check(tb)) { 458 | PyErr_SetString(PyExc_TypeError, "throw() third argument must be a traceback object"); 459 | return NULL; 460 | } 461 | 462 | Py_INCREF(typ); 463 | Py_XINCREF(val); 464 | Py_XINCREF(tb); 465 | 466 | if (PyExceptionClass_Check(typ)) { 467 | PyErr_NormalizeException(&typ, &val, &tb); 468 | } else if (PyExceptionInstance_Check(typ)) { 469 | /* Raising an instance. The value should be a dummy. */ 470 | if (val && val != Py_None) { 471 | PyErr_SetString(PyExc_TypeError, "instance exceptions cannot have a separate value"); 472 | goto error; 473 | } else { 474 | /* Normalize to raise , */ 475 | Py_XDECREF(val); 476 | val = typ; 477 | typ = PyExceptionInstance_Class(typ); 478 | Py_INCREF(typ); 479 | } 480 | } else { 481 | /* Not something you can raise. throw() fails. */ 482 | PyErr_Format(PyExc_TypeError, "exceptions must be classes, or instances, not %s", Py_TYPE(typ)->tp_name); 483 | goto error; 484 | } 485 | 486 | if (!(current = get_current())) { 487 | goto error; 488 | } 489 | 490 | if (self == current) { 491 | PyErr_SetString(PyExc_FiberError, "cannot throw from a Fiber to itself"); 492 | goto error; 493 | } 494 | 495 | if (self->stacklet_h == EMPTY_STACKLET_HANDLE) { 496 | PyErr_SetString(PyExc_FiberError, "Fiber has ended"); 497 | goto error; 498 | } 499 | 500 | if (self->thread_h != current->thread_h) { 501 | PyErr_SetString(PyExc_FiberError, "cannot switch to a Fiber on a different thread"); 502 | return NULL; 503 | } 504 | 505 | /* set error and do a switch with NULL as the value */ 506 | PyErr_Restore(typ, val, tb); 507 | 508 | return do_switch(self, NULL); 509 | 510 | error: 511 | /* Didn't use our arguments, so restore their original refcounts */ 512 | Py_DECREF(typ); 513 | Py_XDECREF(val); 514 | Py_XDECREF(tb); 515 | return NULL; 516 | } 517 | 518 | 519 | static PyObject * 520 | Fiber_func_is_alive(Fiber *self) 521 | { 522 | Fiber *current; 523 | 524 | if (!(current = get_current())) { 525 | return NULL; 526 | } 527 | 528 | /* self->stacklet_h is only valid when self is not currently running */ 529 | return PyBool_FromLong(current == self || self->stacklet_h != EMPTY_STACKLET_HANDLE); 530 | } 531 | 532 | 533 | static PyObject * 534 | Fiber_func_getstate(Fiber *self) 535 | { 536 | PyErr_Format(PyExc_TypeError, "cannot serialize '%s' object", Py_TYPE(self)->tp_name); 537 | return NULL; 538 | } 539 | 540 | 541 | static PyObject * 542 | Fiber_dict_get(Fiber *self, void* c) 543 | { 544 | UNUSED_ARG(c); 545 | 546 | if (self->dict == NULL) { 547 | self->dict = PyDict_New(); 548 | if (self->dict == NULL) { 549 | return NULL; 550 | } 551 | } 552 | Py_INCREF(self->dict); 553 | return self->dict; 554 | } 555 | 556 | 557 | static int 558 | Fiber_dict_set(Fiber *self, PyObject* val, void* c) 559 | { 560 | PyObject* tmp; 561 | UNUSED_ARG(c); 562 | 563 | if (val == NULL) { 564 | PyErr_SetString(PyExc_AttributeError, "__dict__ may not be deleted"); 565 | return -1; 566 | } 567 | if (!PyDict_Check(val)) { 568 | PyErr_SetString(PyExc_TypeError, "__dict__ must be a dictionary"); 569 | return -1; 570 | } 571 | tmp = self->dict; 572 | Py_INCREF(val); 573 | self->dict = val; 574 | Py_XDECREF(tmp); 575 | return 0; 576 | } 577 | 578 | 579 | static PyObject * 580 | Fiber_parent_get(Fiber *self, void* c) 581 | { 582 | PyObject *result; 583 | UNUSED_ARG(c); 584 | 585 | if (self->parent) { 586 | result = (PyObject *)self->parent; 587 | } else { 588 | result = Py_None; 589 | } 590 | Py_INCREF(result); 591 | return result; 592 | } 593 | 594 | 595 | static int 596 | Fiber_parent_set(Fiber *self, PyObject *val, void* c) 597 | { 598 | Fiber *p, *nparent; 599 | UNUSED_ARG(c); 600 | 601 | if (val == NULL) { 602 | PyErr_SetString(PyExc_AttributeError, "can't delete attribute"); 603 | return -1; 604 | } 605 | 606 | if (!PyObject_TypeCheck(val, &FiberType)) { 607 | PyErr_SetString(PyExc_TypeError, "parent must be a Fiber"); 608 | return -1; 609 | } 610 | 611 | nparent = (Fiber *)val; 612 | for (p = nparent; p != NULL; p = p->parent) { 613 | if (p == self) { 614 | PyErr_SetString(PyExc_ValueError, "cyclic parent chain"); 615 | return -1; 616 | } 617 | } 618 | 619 | if (nparent->stacklet_h == EMPTY_STACKLET_HANDLE) { 620 | PyErr_SetString(PyExc_ValueError, "parent must not have ended"); 621 | return -1; 622 | } 623 | 624 | if (nparent->thread_h != self->thread_h) { 625 | PyErr_SetString(PyExc_ValueError, "parent cannot be on a different thread"); 626 | return -1; 627 | } 628 | 629 | p = self->parent; 630 | self->parent = nparent; 631 | Py_INCREF(nparent); 632 | Py_XDECREF(p); 633 | 634 | return 0; 635 | } 636 | 637 | 638 | static int 639 | Fiber_tp_traverse(Fiber *self, visitproc visit, void *arg) 640 | { 641 | Py_VISIT(self->target); 642 | Py_VISIT(self->args); 643 | Py_VISIT(self->kwargs); 644 | Py_VISIT(self->dict); 645 | Py_VISIT(self->ts_dict); 646 | Py_VISIT(self->parent); 647 | Py_VISIT(self->ts.frame); 648 | Py_VISIT(self->ts.exc_state.exc_value); 649 | #if PY_MINOR_VERSION < 11 650 | Py_VISIT(self->ts.exc_state.exc_type); 651 | Py_VISIT(self->ts.exc_state.exc_traceback); 652 | #endif 653 | Py_VISIT(self->ts.exc_state.previous_item); 654 | 655 | return 0; 656 | } 657 | 658 | 659 | static int 660 | Fiber_tp_clear(Fiber *self) 661 | { 662 | Py_CLEAR(self->target); 663 | Py_CLEAR(self->args); 664 | Py_CLEAR(self->kwargs); 665 | Py_CLEAR(self->dict); 666 | Py_CLEAR(self->ts_dict); 667 | Py_CLEAR(self->parent); 668 | Py_CLEAR(self->ts.frame); 669 | Py_CLEAR(self->ts.exc_state.exc_value); 670 | #if PY_MINOR_VERSION < 11 671 | Py_CLEAR(self->ts.exc_state.exc_type); 672 | Py_CLEAR(self->ts.exc_state.exc_traceback); 673 | #endif 674 | Py_CLEAR(self->ts.exc_state.previous_item); 675 | 676 | return 0; 677 | } 678 | 679 | 680 | static void 681 | Fiber_tp_dealloc(Fiber *self) 682 | { 683 | if (self->stacklet_h != NULL && self->stacklet_h != EMPTY_STACKLET_HANDLE) { 684 | stacklet_destroy(self->stacklet_h); 685 | self->stacklet_h = NULL; 686 | } 687 | if (self->is_main) { 688 | stacklet_deletethread(self->thread_h); 689 | self->thread_h = NULL; 690 | } 691 | if (self->weakreflist != NULL) { 692 | PyObject_ClearWeakRefs((PyObject *)self); 693 | } 694 | Py_TYPE(self)->tp_clear((PyObject *)self); 695 | Py_TYPE(self)->tp_free((PyObject *)self); 696 | } 697 | 698 | 699 | static PyMethodDef 700 | Fiber_tp_methods[] = { 701 | { "current", (PyCFunction)fibers_func_current, METH_CLASS|METH_NOARGS, "Returns the current Fiber" }, 702 | { "is_alive", (PyCFunction)Fiber_func_is_alive, METH_NOARGS, "Returns true if the Fiber can still be switched to" }, 703 | { "switch", (PyCFunction)Fiber_func_switch, METH_VARARGS, "Switch execution to this Fiber" }, 704 | { "throw", (PyCFunction)Fiber_func_throw, METH_VARARGS, "Switch execution and raise the specified exception to this Fiber" }, 705 | { "__getstate__", (PyCFunction)Fiber_func_getstate, METH_NOARGS, "Serialize the Fiber object, not really" }, 706 | { NULL } 707 | }; 708 | 709 | 710 | static PyGetSetDef Fiber_tp_getsets[] = { 711 | {"__dict__", (getter)Fiber_dict_get, (setter)Fiber_dict_set, "Instance dictionary", NULL}, 712 | {"parent", (getter)Fiber_parent_get, (setter)Fiber_parent_set, "Fiber parent or None if it's the main Fiber", NULL}, 713 | {NULL} 714 | }; 715 | 716 | 717 | static PyTypeObject FiberType = { 718 | PyVarObject_HEAD_INIT(NULL, 0) 719 | "fibers._cfibers.Fiber", /*tp_name*/ 720 | sizeof(Fiber), /*tp_basicsize*/ 721 | 0, /*tp_itemsize*/ 722 | (destructor)Fiber_tp_dealloc, /*tp_dealloc*/ 723 | 0, /*tp_print*/ 724 | 0, /*tp_getattr*/ 725 | 0, /*tp_setattr*/ 726 | 0, /*tp_compare*/ 727 | 0, /*tp_repr*/ 728 | 0, /*tp_as_number*/ 729 | 0, /*tp_as_sequence*/ 730 | 0, /*tp_as_mapping*/ 731 | 0, /*tp_hash */ 732 | 0, /*tp_call*/ 733 | 0, /*tp_str*/ 734 | 0, /*tp_getattro*/ 735 | 0, /*tp_setattro*/ 736 | 0, /*tp_as_buffer*/ 737 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /*tp_flags*/ 738 | 0, /*tp_doc*/ 739 | (traverseproc)Fiber_tp_traverse, /*tp_traverse*/ 740 | (inquiry)Fiber_tp_clear, /*tp_clear*/ 741 | 0, /*tp_richcompare*/ 742 | offsetof(Fiber, weakreflist), /*tp_weaklistoffset*/ 743 | 0, /*tp_iter*/ 744 | 0, /*tp_iternext*/ 745 | Fiber_tp_methods, /*tp_methods*/ 746 | 0, /*tp_members*/ 747 | Fiber_tp_getsets, /*tp_getsets*/ 748 | 0, /*tp_base*/ 749 | 0, /*tp_dict*/ 750 | 0, /*tp_descr_get*/ 751 | 0, /*tp_descr_set*/ 752 | offsetof(Fiber, dict), /*tp_dictoffset*/ 753 | (initproc)Fiber_tp_init, /*tp_init*/ 754 | 0, /*tp_alloc*/ 755 | Fiber_tp_new, /*tp_new*/ 756 | }; 757 | 758 | 759 | static PyMethodDef 760 | fibers_methods[] = { 761 | { "current", (PyCFunction)fibers_func_current, METH_NOARGS, "Get the current Fiber" }, 762 | { NULL } 763 | }; 764 | 765 | 766 | static PyModuleDef fibers_module = { 767 | PyModuleDef_HEAD_INIT, 768 | "fibers._cfibers", /*m_name*/ 769 | NULL, /*m_doc*/ 770 | -1, /*m_size*/ 771 | fibers_methods, /*m_methods*/ 772 | }; 773 | 774 | 775 | /* Module */ 776 | PyMODINIT_FUNC 777 | PyInit__cfibers(void) 778 | { 779 | PyObject *fibers; 780 | 781 | /* Main module */ 782 | fibers = PyModule_Create(&fibers_module); 783 | 784 | /* keys for per-thread dictionary */ 785 | main_fiber_key = PyUnicode_InternFromString("__fibers_main"); 786 | current_fiber_key = PyUnicode_InternFromString("__fibers_current"); 787 | 788 | if ((current_fiber_key == NULL) || (main_fiber_key == NULL)) { 789 | goto fail; 790 | } 791 | 792 | /* Exceptions */ 793 | PyExc_FiberError = PyErr_NewException("fibers._cfibers.error", NULL, NULL); 794 | MyPyModule_AddType(fibers, "error", (PyTypeObject *)PyExc_FiberError); 795 | 796 | /* Types */ 797 | MyPyModule_AddType(fibers, "Fiber", &FiberType); 798 | 799 | return fibers; 800 | 801 | fail: 802 | Py_DECREF(fibers); 803 | 804 | return NULL; 805 | } 806 | -------------------------------------------------------------------------------- /src/fibers.h: -------------------------------------------------------------------------------- 1 | #ifndef PYFIBERS_H 2 | #define PYFIBERS_H 3 | 4 | /* python */ 5 | #define PY_SSIZE_T_CLEAN 6 | #include "Python.h" 7 | 8 | /* stacklet */ 9 | #include "stacklet.h" 10 | 11 | /* Custom types */ 12 | typedef int Bool; 13 | #define True 1 14 | #define False 0 15 | 16 | #define UNUSED_ARG(arg) (void)arg 17 | 18 | /* Python types */ 19 | typedef struct _fiber { 20 | PyObject_HEAD 21 | PyObject *ts_dict; 22 | PyObject *dict; 23 | PyObject *weakreflist; 24 | struct _fiber *parent; 25 | stacklet_thread_handle thread_h; 26 | stacklet_handle stacklet_h; 27 | Bool initialized; 28 | Bool is_main; 29 | PyObject *target; 30 | PyObject *args; 31 | PyObject *kwargs; 32 | struct { 33 | #if PY_MINOR_VERSION >= 11 34 | _PyCFrame *cframe; 35 | _PyStackChunk *datastack_chunk; 36 | PyObject **datastack_top; 37 | PyObject **datastack_limit; 38 | #endif 39 | struct _frame *frame; 40 | int recursion_depth; 41 | #if PY_MINOR_VERSION >= 12 42 | int c_recursion_remaining; 43 | #endif 44 | _PyErr_StackItem exc_state; 45 | } ts; 46 | } Fiber; 47 | 48 | static PyTypeObject FiberType; 49 | 50 | 51 | /* Some helper stuff */ 52 | #ifdef _MSC_VER 53 | #define INLINE __inline 54 | #else 55 | #define INLINE inline 56 | #endif 57 | 58 | #define ASSERT(x) \ 59 | do { \ 60 | if (!(x)) { \ 61 | fprintf (stderr, "%s:%u: Assertion `" #x "' failed.\n", \ 62 | __FILE__, __LINE__); \ 63 | abort(); \ 64 | } \ 65 | } while(0) \ 66 | 67 | 68 | /* Add a type to a module */ 69 | static int 70 | MyPyModule_AddType(PyObject *module, const char *name, PyTypeObject *type) 71 | { 72 | if (PyType_Ready(type)) { 73 | return -1; 74 | } 75 | Py_INCREF(type); 76 | if (PyModule_AddObject(module, name, (PyObject *)type)) { 77 | Py_DECREF(type); 78 | return -1; 79 | } 80 | return 0; 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /src/slp_platformselect.h: -------------------------------------------------------------------------------- 1 | #if defined(__MINGW32__) 2 | #include "switch_x86_gcc.h" /* gcc on X86 */ 3 | #elif defined(_M_IX86) 4 | #include "switch_x86_msvc.h" /* MS Visual Studio on X86 */ 5 | #elif defined(_M_X64) 6 | #include "switch_x64_msvc.h" /* MS Visual Studio on X64 */ 7 | #elif defined(__GNUC__) && defined(__amd64__) 8 | #include "switch_x86_64_gcc.h" /* gcc on amd64 */ 9 | #elif defined(__GNUC__) && defined(__i386__) 10 | #include "switch_x86_gcc.h" /* gcc on X86 */ 11 | #elif defined(__GNUC__) && defined(__arm__) 12 | #include "switch_arm_gcc.h" /* gcc on arm */ 13 | #elif defined(__GNUC__) && defined(__aarch64__) 14 | #include "switch_aarch64_gcc.h" /* gcc on aarch64 */ 15 | #elif defined(__GNUC__) && defined(__PPC64__) 16 | #include "switch_ppc64_gcc.h" /* gcc on ppc64 */ 17 | #elif defined(__GNUC__) && defined(__mips__) && defined(_ABI64) 18 | #include "switch_mips64_gcc.h" /* gcc on mips64 */ 19 | #elif defined(__GNUC__) && defined(__s390x__) 20 | #include "switch_s390x_gcc.h" 21 | #else 22 | #error "Unsupported platform!" 23 | #endif 24 | -------------------------------------------------------------------------------- /src/stacklet.c: -------------------------------------------------------------------------------- 1 | /********** A really minimal coroutine package for C ********** 2 | * By Armin Rigo 3 | */ 4 | 5 | #include "stacklet.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | /************************************************************ 12 | * platform specific code 13 | */ 14 | 15 | /* The default stack direction is downwards, 0, but platforms 16 | * can redefine it to upwards growing, 1. 17 | */ 18 | #define STACK_DIRECTION 0 19 | #define STATIC_NOINLINE __attribute__((noinline)) static 20 | 21 | #include "slp_platformselect.h" 22 | 23 | #if STACK_DIRECTION != 0 24 | # error "review this whole code, which depends on STACK_DIRECTION==0 so far" 25 | #endif 26 | 27 | /************************************************************/ 28 | 29 | /* #define DEBUG_DUMP */ 30 | 31 | #ifdef DEBUG_DUMP 32 | #include 33 | #endif 34 | 35 | /************************************************************/ 36 | 37 | struct stacklet_s { 38 | /* The portion of the real stack claimed by this paused stacklet. */ 39 | char *stack_start; /* the "near" end of the stack */ 40 | char *stack_stop; /* the "far" end of the stack */ 41 | 42 | /* The amount that has been saved away so far, just after this struct. 43 | * There is enough allocated space for 'stack_stop - stack_start' 44 | * bytes. 45 | */ 46 | ptrdiff_t stack_saved; /* the amount saved */ 47 | 48 | /* Internally, some stacklets are arranged in a list, to handle lazy 49 | * saving of stacks: if the stacklet has a partially unsaved stack, 50 | * this points to the next stacklet with a partially unsaved stack, 51 | * creating a linked list with each stacklet's stack_stop higher 52 | * than the previous one. The last entry in the list is always the 53 | * main stack. 54 | */ 55 | struct stacklet_s *stack_prev; 56 | 57 | stacklet_thread_handle stack_thrd; /* the thread where the stacklet is */ 58 | }; 59 | 60 | struct stacklet_thread_s { 61 | struct stacklet_s *g_stack_chain_head; /* NULL <=> running main */ 62 | char *g_current_stack_stop; 63 | char *g_current_stack_marker; 64 | struct stacklet_s *g_source; 65 | struct stacklet_s *g_target; 66 | }; 67 | 68 | #define _check(x) do { if (!(x)) _check_failed(#x); } while (0) 69 | 70 | static void _check_failed(const char *check) 71 | { 72 | fprintf(stderr, "FATAL: stacklet: %s failed\n", check); 73 | abort(); 74 | } 75 | 76 | static void check_valid(struct stacklet_s *g) 77 | { 78 | _check(g->stack_saved >= 0); 79 | } 80 | 81 | /***************************************************************/ 82 | 83 | static void g_save(struct stacklet_s* g, char* stop 84 | #ifdef DEBUG_DUMP 85 | , int overwrite_stack_for_debug 86 | #endif 87 | ) 88 | { 89 | /* Save more of g's stack into the heap -- at least up to 'stop' 90 | 91 | In the picture below, the C stack is on the left, growing down, 92 | and the C heap on the right. The area marked with xxx is the logical 93 | stack of the stacklet 'g'. It can be half in the C stack (its older 94 | part), and half in the heap (its newer part). 95 | 96 | g->stack_stop |________| 97 | |xxxxxxxx| 98 | |xxx __ stop ......... 99 | |xxxxxxxx| ==> : : 100 | |________| :_______: 101 | | | |xxxxxxx| 102 | | | |xxxxxxx| 103 | g->stack_start | | |_______| g+1 104 | 105 | */ 106 | ptrdiff_t sz1 = g->stack_saved; 107 | ptrdiff_t sz2 = stop - g->stack_start; 108 | check_valid(g); 109 | _check(stop <= g->stack_stop); 110 | 111 | if (sz2 > sz1) { 112 | char *c = (char *)(g + 1); 113 | #if STACK_DIRECTION == 0 114 | memcpy(c+sz1, g->stack_start+sz1, sz2-sz1); 115 | # ifdef DEBUG_DUMP 116 | if (overwrite_stack_for_debug) 117 | memset(g->stack_start+sz1, 0xdb, sz2-sz1); 118 | # endif 119 | #else 120 | xxx; 121 | #endif 122 | g->stack_saved = sz2; 123 | } 124 | } 125 | 126 | /* Allocate and store in 'g_source' a new stacklet, which has the C 127 | * stack from 'old_stack_pointer' to 'g_current_stack_stop'. It is 128 | * initially completely unsaved, so it is attached to the head of the 129 | * chained list of 'stack_prev'. 130 | */ 131 | static int g_allocate_source_stacklet(void *old_stack_pointer, 132 | struct stacklet_thread_s *thrd) 133 | { 134 | struct stacklet_s *stacklet; 135 | ptrdiff_t stack_size = (thrd->g_current_stack_stop - 136 | (char *)old_stack_pointer); 137 | 138 | thrd->g_source = malloc(sizeof(struct stacklet_s) + stack_size); 139 | if (thrd->g_source == NULL) 140 | return -1; 141 | 142 | stacklet = thrd->g_source; 143 | stacklet->stack_start = old_stack_pointer; 144 | stacklet->stack_stop = thrd->g_current_stack_stop; 145 | stacklet->stack_saved = 0; 146 | stacklet->stack_prev = thrd->g_stack_chain_head; 147 | stacklet->stack_thrd = thrd; 148 | thrd->g_stack_chain_head = stacklet; 149 | return 0; 150 | } 151 | 152 | /* Save more of the C stack away, up to 'target_stop'. 153 | */ 154 | static void g_clear_stack(struct stacklet_s *g_target, 155 | struct stacklet_thread_s *thrd) 156 | { 157 | struct stacklet_s *current = thrd->g_stack_chain_head; 158 | char *target_stop = g_target->stack_stop; 159 | check_valid(g_target); 160 | 161 | /* save and unlink stacklets that are completely within 162 | the area to clear. */ 163 | while (current != NULL && current->stack_stop <= target_stop) { 164 | struct stacklet_s *prev = current->stack_prev; 165 | check_valid(current); 166 | current->stack_prev = NULL; 167 | if (current != g_target) { 168 | /* don't bother saving away g_target, because 169 | it would be immediately restored */ 170 | g_save(current, current->stack_stop 171 | #ifdef DEBUG_DUMP 172 | , 1 173 | #endif 174 | ); 175 | } 176 | current = prev; 177 | } 178 | 179 | /* save a partial stack */ 180 | if (current != NULL && current->stack_start < target_stop) 181 | g_save(current, target_stop 182 | #ifdef DEBUG_DUMP 183 | , 1 184 | #endif 185 | ); 186 | 187 | thrd->g_stack_chain_head = current; 188 | } 189 | 190 | /* This saves the current state in a new stacklet that gets stored in 191 | * 'g_source', and save away enough of the stack to allow a jump to 192 | * 'g_target'. 193 | */ 194 | static void *g_save_state(void *old_stack_pointer, void *rawthrd) 195 | { 196 | struct stacklet_thread_s *thrd = (struct stacklet_thread_s *)rawthrd; 197 | if (g_allocate_source_stacklet(old_stack_pointer, thrd) < 0) 198 | return NULL; 199 | g_clear_stack(thrd->g_target, thrd); 200 | return thrd->g_target->stack_start; 201 | } 202 | 203 | /* This saves the current state in a new stacklet that gets stored in 204 | * 'g_source', but returns NULL, to not do any restoring yet. 205 | */ 206 | static void *g_initial_save_state(void *old_stack_pointer, void *rawthrd) 207 | { 208 | struct stacklet_thread_s *thrd = (struct stacklet_thread_s *)rawthrd; 209 | if (g_allocate_source_stacklet(old_stack_pointer, thrd) == 0) 210 | g_save(thrd->g_source, thrd->g_current_stack_marker 211 | #ifdef DEBUG_DUMP 212 | , 0 213 | #endif 214 | ); 215 | return NULL; 216 | } 217 | 218 | /* Save away enough of the stack to allow a jump to 'g_target'. 219 | */ 220 | static void *g_destroy_state(void *old_stack_pointer, void *rawthrd) 221 | { 222 | struct stacklet_thread_s *thrd = (struct stacklet_thread_s *)rawthrd; 223 | thrd->g_source = EMPTY_STACKLET_HANDLE; 224 | g_clear_stack(thrd->g_target, thrd); 225 | return thrd->g_target->stack_start; 226 | } 227 | 228 | /* Restore the C stack by copying back from the heap in 'g_target', 229 | * and free 'g_target'. 230 | */ 231 | static void *g_restore_state(void *new_stack_pointer, void *rawthrd) 232 | { 233 | /* Restore the heap copy back into the C stack */ 234 | struct stacklet_thread_s *thrd = (struct stacklet_thread_s *)rawthrd; 235 | struct stacklet_s *g = thrd->g_target; 236 | ptrdiff_t stack_saved = g->stack_saved; 237 | check_valid(g); 238 | 239 | _check(new_stack_pointer == g->stack_start); 240 | #if STACK_DIRECTION == 0 241 | memcpy(g->stack_start, g+1, stack_saved); 242 | #else 243 | memcpy(g->stack_start - stack_saved, g+1, stack_saved); 244 | #endif 245 | thrd->g_current_stack_stop = g->stack_stop; 246 | g->stack_saved = -13; /* debugging */ 247 | free(g); 248 | return EMPTY_STACKLET_HANDLE; 249 | } 250 | 251 | STATIC_NOINLINE 252 | void *_stacklet_switchstack(void *(*save_state)(void*, void*), 253 | void *(*restore_state)(void*, void*), 254 | void *extra) 255 | { 256 | return slp_switch(save_state, restore_state, extra); 257 | } 258 | 259 | STATIC_NOINLINE 260 | void g_initialstub(struct stacklet_thread_s *thrd, 261 | stacklet_run_fn run, void *run_arg) 262 | { 263 | struct stacklet_s *result; 264 | 265 | /* The following call returns twice! */ 266 | result = (struct stacklet_s *) _stacklet_switchstack(g_initial_save_state, 267 | g_restore_state, 268 | thrd); 269 | if (result == NULL && thrd->g_source != NULL) { 270 | /* First time it returns. Only g_initial_save_state() has run 271 | and has created 'g_source'. Call run(). */ 272 | thrd->g_current_stack_stop = thrd->g_current_stack_marker; 273 | result = run(thrd->g_source, run_arg); 274 | 275 | /* Then switch to 'result'. */ 276 | check_valid(result); 277 | thrd->g_target = result; 278 | _stacklet_switchstack(g_destroy_state, g_restore_state, thrd); 279 | 280 | _check_failed("we should not return here"); 281 | abort(); 282 | } 283 | /* The second time it returns. */ 284 | } 285 | 286 | /************************************************************/ 287 | 288 | stacklet_thread_handle stacklet_newthread(void) 289 | { 290 | struct stacklet_thread_s *thrd; 291 | 292 | thrd = malloc(sizeof(struct stacklet_thread_s)); 293 | if (thrd != NULL) 294 | memset(thrd, 0, sizeof(struct stacklet_thread_s)); 295 | return thrd; 296 | } 297 | 298 | void stacklet_deletethread(stacklet_thread_handle thrd) 299 | { 300 | free(thrd); 301 | } 302 | 303 | stacklet_handle stacklet_new(stacklet_thread_handle thrd, 304 | stacklet_run_fn run, void *run_arg) 305 | { 306 | long stackmarker; 307 | _check((char *)NULL < (char *)&stackmarker); 308 | if (thrd->g_current_stack_stop <= (char *)&stackmarker) 309 | thrd->g_current_stack_stop = ((char *)&stackmarker) + 1; 310 | 311 | thrd->g_current_stack_marker = (char *)&stackmarker; 312 | g_initialstub(thrd, run, run_arg); 313 | return thrd->g_source; 314 | } 315 | 316 | stacklet_handle stacklet_switch(stacklet_handle target) 317 | { 318 | long stackmarker; 319 | stacklet_thread_handle thrd = target->stack_thrd; 320 | check_valid(target); 321 | if (thrd->g_current_stack_stop <= (char *)&stackmarker) 322 | thrd->g_current_stack_stop = ((char *)&stackmarker) + 1; 323 | 324 | thrd->g_target = target; 325 | _stacklet_switchstack(g_save_state, g_restore_state, thrd); 326 | return thrd->g_source; 327 | } 328 | 329 | void stacklet_destroy(stacklet_handle target) 330 | { 331 | check_valid(target); 332 | if (target->stack_prev != NULL) { 333 | /* 'target' appears to be in the chained list 'unsaved_stack', 334 | so remove it from there. Note that if 'thrd' was already 335 | deleted, it means that we left the thread and all stacklets 336 | still in the thread should be fully copied away from the 337 | stack --- so should have stack_prev == NULL. In this case 338 | we don't even read 'stack_thrd', already deallocated. */ 339 | stacklet_thread_handle thrd = target->stack_thrd; 340 | struct stacklet_s **pp = &thrd->g_stack_chain_head; 341 | for (; *pp != NULL; pp = &(*pp)->stack_prev) { 342 | check_valid(*pp); 343 | if (*pp == target) { 344 | *pp = target->stack_prev; 345 | break; 346 | } 347 | } 348 | } 349 | target->stack_saved = -11; /* debugging */ 350 | free(target); 351 | } 352 | 353 | char **_stacklet_translate_pointer(stacklet_handle context, char **ptr) 354 | { 355 | char *p = (char *)ptr; 356 | long delta; 357 | if (context == NULL) 358 | return ptr; 359 | check_valid(context); 360 | delta = p - context->stack_start; 361 | if (((unsigned long)delta) < ((unsigned long)context->stack_saved)) { 362 | /* a pointer to a saved away word */ 363 | char *c = (char *)(context + 1); 364 | return (char **)(c + delta); 365 | } 366 | if (((unsigned long)delta) >= 367 | (unsigned long)(context->stack_stop - context->stack_start)) { 368 | /* out-of-stack pointer! it's only ok if we are the main stacklet 369 | and we are reading past the end, because the main stacklet's 370 | stack stop is not exactly known. */ 371 | _check(delta >= 0); 372 | _check(((long)context->stack_stop) & 1); 373 | } 374 | return ptr; 375 | } 376 | -------------------------------------------------------------------------------- /src/stacklet.h: -------------------------------------------------------------------------------- 1 | /********** A really minimal coroutine package for C **********/ 2 | #ifndef _STACKLET_H_ 3 | #define _STACKLET_H_ 4 | 5 | #include 6 | 7 | 8 | /* A "stacklet handle" is an opaque pointer to a suspended stack. 9 | * Whenever we suspend the current stack in order to switch elsewhere, 10 | * stacklet.c passes to the target a 'stacklet_handle' argument that points 11 | * to the original stack now suspended. The handle must later be passed 12 | * back to this API once, in order to resume the stack. It is only 13 | * valid once. 14 | */ 15 | typedef struct stacklet_s *stacklet_handle; 16 | 17 | #define EMPTY_STACKLET_HANDLE ((stacklet_handle) -1) 18 | 19 | 20 | /* Multithread support. 21 | */ 22 | typedef struct stacklet_thread_s *stacklet_thread_handle; 23 | 24 | stacklet_thread_handle stacklet_newthread(void); 25 | void stacklet_deletethread(stacklet_thread_handle thrd); 26 | 27 | 28 | /* The "run" function of a stacklet. The first argument is the handle 29 | * of the stack from where we come. When such a function returns, it 30 | * must return a (non-empty) stacklet_handle that tells where to go next. 31 | */ 32 | typedef stacklet_handle (*stacklet_run_fn)(stacklet_handle, void *); 33 | 34 | /* Call 'run(source, run_arg)' in a new stack. See stacklet_switch() 35 | * for the return value. 36 | */ 37 | stacklet_handle stacklet_new(stacklet_thread_handle thrd, 38 | stacklet_run_fn run, void *run_arg); 39 | 40 | /* Switch to the target handle, resuming its stack. This returns: 41 | * - if we come back from another call to stacklet_switch(), the source handle 42 | * - if we come back from a run() that finishes, EMPTY_STACKLET_HANDLE 43 | * - if we run out of memory, NULL 44 | * Don't call this with an already-used target, with EMPTY_STACKLET_HANDLE, 45 | * or with a stack handle from another thread (in multithreaded apps). 46 | */ 47 | stacklet_handle stacklet_switch(stacklet_handle target); 48 | 49 | /* Delete a stack handle without resuming it at all. 50 | * (This works even if the stack handle is of a different thread) 51 | */ 52 | void stacklet_destroy(stacklet_handle target); 53 | 54 | /* stacklet_handle _stacklet_switch_to_copy(stacklet_handle) --- later */ 55 | 56 | /* Hack: translate a pointer into the stack of a stacklet into a pointer 57 | * to where it is really stored so far. Only to access word-sized data. 58 | */ 59 | char **_stacklet_translate_pointer(stacklet_handle context, char **ptr); 60 | 61 | #endif /* _STACKLET_H_ */ 62 | -------------------------------------------------------------------------------- /src/switch_aarch64_gcc.h: -------------------------------------------------------------------------------- 1 | 2 | static void *slp_switch(void *(*save_state)(void*, void*), 3 | void *(*restore_state)(void*, void*), 4 | void *extra) __attribute__((noinline)); 5 | 6 | static void *slp_switch(void *(*save_state)(void*, void*), 7 | void *(*restore_state)(void*, void*), 8 | void *extra) 9 | { 10 | void *result; 11 | /* 12 | registers to preserve: x18-x28, x29(fp), and v8-v15 13 | registers marked as clobbered: x0-x18, x30 14 | 15 | Note that x18 appears in both lists; see below. We also save 16 | x30 although it's also marked as clobbered, which might not 17 | be necessary but doesn't hurt. 18 | 19 | Don't assume gcc saves any register for us when generating 20 | code for slp_switch(). 21 | 22 | The values 'save_state', 'restore_state' and 'extra' are first moved 23 | by gcc to some registers that are not marked as clobbered, so between 24 | x19 and x29. Similarly, gcc expects 'result' to be in a register 25 | between x19 and x29. We don't want x18 to be used here, because of 26 | some special meaning it might have. We don't want x30 to be used 27 | here, because it is clobbered by the first "blr". 28 | 29 | This means that three of the values we happen to save and restore 30 | will, in fact, contain the three arguments, and one of these values 31 | will, in fact, not be restored at all but receive 'result'. 32 | */ 33 | 34 | __asm__ volatile ( 35 | 36 | /* The stack is supposed to be aligned as necessary already. 37 | Save 12 registers from x18 to x29, plus 8 from v8 to v15 */ 38 | 39 | "stp x18, x19, [sp, -160]!\n" 40 | "stp x20, x11, [sp, 16]\n" 41 | "stp x22, x23, [sp, 32]\n" 42 | "stp x24, x25, [sp, 48]\n" 43 | "stp x26, x27, [sp, 64]\n" 44 | "stp x28, x29, [sp, 80]\n" 45 | "str d8, [sp, 96]\n" 46 | "str d9, [sp, 104]\n" 47 | "str d10, [sp, 112]\n" 48 | "str d11, [sp, 120]\n" 49 | "str d12, [sp, 128]\n" 50 | "str d13, [sp, 136]\n" 51 | "str d14, [sp, 144]\n" 52 | "str d15, [sp, 152]\n" 53 | 54 | "mov x0, sp\n" /* arg 1: current (old) stack pointer */ 55 | "mov x1, %[extra]\n" /* arg 2: extra, from x19-x28 */ 56 | "blr %[save_state]\n" /* call save_state(), from x19-x28 */ 57 | 58 | /* skip the rest if the return value is null */ 59 | "cbz x0, 0f\n" 60 | 61 | "mov sp, x0\n" /* change the stack pointer */ 62 | 63 | /* From now on, the stack pointer is modified, but the content of the 64 | stack is not restored yet. It contains only garbage here. */ 65 | "mov x1, %[extra]\n" /* arg 2: extra, still from x19-x28 */ 66 | /* arg 1: current (new) stack pointer is already in x0*/ 67 | "blr %[restore_state]\n"/* call restore_state() */ 68 | 69 | /* The stack's content is now restored. */ 70 | "0:\n" 71 | 72 | /* Restore all saved registers */ 73 | "ldp x20, x11, [sp, 16]\n" 74 | "ldp x22, x23, [sp, 32]\n" 75 | "ldp x24, x25, [sp, 48]\n" 76 | "ldp x26, x27, [sp, 64]\n" 77 | "ldp x28, x29, [sp, 80]\n" 78 | "ldr d8, [sp, 96]\n" 79 | "ldr d9, [sp, 104]\n" 80 | "ldr d10, [sp, 112]\n" 81 | "ldr d11, [sp, 120]\n" 82 | "ldr d12, [sp, 128]\n" 83 | "ldr d13, [sp, 136]\n" 84 | "ldr d14, [sp, 144]\n" 85 | "ldr d15, [sp, 152]\n" 86 | "ldp x18, x19, [sp], 160\n" 87 | 88 | /* Move x0 into the final location of 'result' */ 89 | "mov %[result], x0\n" 90 | 91 | : [result]"=r"(result) /* output variables */ 92 | /* input variables */ 93 | : [restore_state]"r"(restore_state), 94 | [save_state]"r"(save_state), 95 | [extra]"r"(extra) 96 | : "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", 97 | "x10", "x11", "x12", "x13", "x14", "x15", "x16", "x17", "x18", 98 | "memory", "cc", "x30" // x30==lr 99 | ); 100 | return result; 101 | } 102 | -------------------------------------------------------------------------------- /src/switch_arm_gcc.h: -------------------------------------------------------------------------------- 1 | #if defined(__ARM_ARCH_4__) || defined (__ARM_ARCH_4T__) 2 | # define call_reg(x) "mov lr, pc ; bx " #x "\n" 3 | #else 4 | /* ARM >= 5 */ 5 | # define call_reg(x) "blx " #x "\n" 6 | #endif 7 | 8 | static void *slp_switch(void *(*save_state)(void*, void*), 9 | void *(*restore_state)(void*, void*), 10 | void *extra) __attribute__((noinline)); 11 | 12 | static void *slp_switch(void *(*save_state)(void*, void*), 13 | void *(*restore_state)(void*, void*), 14 | void *extra) 15 | { 16 | void *result; 17 | /* 18 | seven registers to preserve: r2, r3, r7, r8, r9, r10, r11 19 | registers marked as clobbered: r0, r1, r4, r5, r6, r12, lr 20 | others: r13 is sp; r14 is lr; r15 is pc 21 | */ 22 | 23 | __asm__ volatile ( 24 | 25 | /* align the stack and save 7 more registers explicitly */ 26 | "mov r0, sp\n" 27 | "and r1, r0, #-16\n" 28 | "mov sp, r1\n" 29 | "push {r0, r2, r3, r7, r8, r9, r10, r11}\n" /* total 8, still aligned */ 30 | #ifndef __SOFTFP__ 31 | /* We also push d8-d15 to preserve them explicitly. This assumes 32 | * that this code is in a function that doesn't use floating-point 33 | * at all, and so don't touch the "d" registers (that's why we mark 34 | * it as non-inlinable). So here by pushing/popping d8-d15 we are 35 | * saving precisely the callee-saved registers in all cases. We 36 | * could also try to list all "d" registers as clobbered, but it 37 | * doesn't work: there is no way I could find to know if we have 16 38 | * or 32 "d" registers (depends on the exact -mcpu=... and we don't 39 | * know it from the C code). If we have 32, then gcc would "save" 40 | * d8-d15 by copying them into d16-d23 for example, and it doesn't 41 | * work. */ 42 | "vpush {d8, d9, d10, d11, d12, d13, d14, d15}\n" /* 16 words, still aligned */ 43 | #endif 44 | 45 | /* save values in callee saved registers for later */ 46 | "mov r4, %[restore_state]\n" /* can't be r0 or r1: marked clobbered */ 47 | "mov r5, %[extra]\n" /* can't be r0 or r1 or r4: marked clob. */ 48 | "mov r3, %[save_state]\n" /* can't be r0, r1, r4, r5: marked clob. */ 49 | "mov r0, sp\n" /* arg 1: current (old) stack pointer */ 50 | "mov r1, r5\n" /* arg 2: extra */ 51 | call_reg(r3) /* call save_state() */ 52 | 53 | /* skip the rest if the return value is null */ 54 | "cmp r0, #0\n" 55 | "beq zero\n" 56 | 57 | "mov sp, r0\n" /* change the stack pointer */ 58 | 59 | /* From now on, the stack pointer is modified, but the content of the 60 | stack is not restored yet. It contains only garbage here. */ 61 | "mov r1, r5\n" /* arg 2: extra */ 62 | /* arg 1: current (new) stack pointer is already in r0*/ 63 | call_reg(r4) /* call restore_state() */ 64 | 65 | /* The stack's content is now restored. */ 66 | "zero:\n" 67 | 68 | #ifndef __SOFTFP__ 69 | "vpop {d8, d9, d10, d11, d12, d13, d14, d15}\n" 70 | #endif 71 | "pop {r1, r2, r3, r7, r8, r9, r10, r11}\n" 72 | "mov sp, r1\n" 73 | "mov %[result], r0\n" 74 | 75 | : [result]"=r"(result) /* output variables */ 76 | /* input variables */ 77 | : [restore_state]"r"(restore_state), 78 | [save_state]"r"(save_state), 79 | [extra]"r"(extra) 80 | : "r0", "r1", "r4", "r5", "r6", "r12", "lr", 81 | "memory", "cc" 82 | ); 83 | return result; 84 | } 85 | -------------------------------------------------------------------------------- /src/switch_mips64_gcc.h: -------------------------------------------------------------------------------- 1 | static void *slp_switch(void *(*save_state)(void*, void*), 2 | void *(*restore_state)(void*, void*), 3 | void *extra) 4 | { 5 | void *result; 6 | __asm__ volatile ( 7 | "daddiu $sp, $sp, -0x50\n" 8 | "sd $s0, 0x0($sp)\n" /* push the registers specified as caller-save */ 9 | "sd $s1, 0x8($sp)\n" 10 | "sd $s2, 0x10($sp)\n" 11 | "sd $s3, 0x18($sp)\n" 12 | "sd $s4, 0x20($sp)\n" 13 | "sd $s5, 0x28($sp)\n" 14 | "sd $s6, 0x30($sp)\n" 15 | "sd $s7, 0x38($sp)\n" 16 | "sd $fp, 0x40($sp)\n" 17 | "sd $ra, 0x48($sp)\n" 18 | 19 | "move $s0, %[rstate]\n" /* save 'restore_state' for later */ 20 | "move $s1, %[extra]\n" /* save 'extra' for later */ 21 | 22 | "move $a1, %[extra]\n"/* arg 2: extra */ 23 | "move $a0, $sp\n" /* arg 1: current (old) stack pointer */ 24 | 25 | "move $t9, %[sstate]\n" 26 | "jalr $t9\n" /* call save_state() */ 27 | 28 | "beqz $v0, 0f\n" /* skip the rest if the return value is null */ 29 | 30 | "move $sp, $v0\n" /* change the stack pointer */ 31 | 32 | /* From now on, the stack pointer is modified, but the content of the 33 | stack is not restored yet. It contains only garbage here. */ 34 | 35 | "move $a1, $s1\n" /* arg 2: extra */ 36 | "move $a0, $v0\n" /* arg 1: current (new) stack pointer */ 37 | "move $t9, $s0\n" 38 | "jalr $t9\n" /* call restore_state() */ 39 | 40 | /* The stack's content is now restored. */ 41 | 42 | "0:\n" 43 | "move %[result], $v0\n" 44 | "ld $s0, 0x0($sp)\n" 45 | "ld $s1, 0x8($sp)\n" 46 | "ld $s2, 0x10($sp)\n" 47 | "ld $s3, 0x18($sp)\n" 48 | "ld $s4, 0x20($sp)\n" 49 | "ld $s5, 0x28($sp)\n" 50 | "ld $s6, 0x30($sp)\n" 51 | "ld $s7, 0x38($sp)\n" 52 | "ld $fp, 0x40($sp)\n" 53 | "ld $ra, 0x48($sp)\n" 54 | "daddiu $sp, $sp, 0x50\n" 55 | 56 | : [result]"=&r"(result) 57 | : [sstate]"r"(save_state), 58 | [rstate]"r"(restore_state), 59 | [extra]"r"(extra) 60 | : "memory", "v0", "a0", "a1", "t9" 61 | ); 62 | return result; 63 | } 64 | -------------------------------------------------------------------------------- /src/switch_ppc64_gcc.h: -------------------------------------------------------------------------------- 1 | #if !(defined(__LITTLE_ENDIAN__) ^ defined(__BIG_ENDIAN__)) 2 | # error "cannot determine if it is ppc64 or ppc64le" 3 | #endif 4 | 5 | #ifdef __BIG_ENDIAN__ 6 | # define TOC_AREA "40" 7 | #else 8 | # define TOC_AREA "24" 9 | #endif 10 | 11 | 12 | /* This depends on these attributes so that gcc generates a function 13 | with no code before the asm, and only "blr" after. */ 14 | static __attribute__((noinline, optimize("O2"))) 15 | void *slp_switch(void *(*save_state)(void*, void*), 16 | void *(*restore_state)(void*, void*), 17 | void *extra) 18 | { 19 | void *result; 20 | __asm__ volatile ( 21 | /* By Vaibhav Sood & Armin Rigo, with some copying from 22 | the Stackless version by Kristjan Valur Jonsson */ 23 | 24 | /* Save all 18 volatile GP registers, 18 volatile FP regs, and 12 25 | volatile vector regs. We need a stack frame of 144 bytes for FPR, 26 | 144 bytes for GPR, 192 bytes for VR plus 48 bytes for the standard 27 | stackframe = 528 bytes (a multiple of 16). */ 28 | 29 | "mflr 0\n" /* Save LR into 16(r1) */ 30 | "std 0, 16(1)\n" 31 | 32 | "std 14,-288(1)\n" /* the GPR save area is between -288(r1) */ 33 | "std 15,-280(1)\n" /* included and -144(r1) excluded */ 34 | "std 16,-272(1)\n" 35 | "std 17,-264(1)\n" 36 | "std 18,-256(1)\n" 37 | "std 19,-248(1)\n" 38 | "std 20,-240(1)\n" 39 | "std 21,-232(1)\n" 40 | "std 22,-224(1)\n" 41 | "std 23,-216(1)\n" 42 | "std 24,-208(1)\n" 43 | "std 25,-200(1)\n" 44 | "std 26,-192(1)\n" 45 | "std 27,-184(1)\n" 46 | "std 28,-176(1)\n" 47 | "std 29,-168(1)\n" 48 | "std 30,-160(1)\n" 49 | "std 31,-152(1)\n" 50 | 51 | "stfd 14,-144(1)\n" /* the FPR save area is between -144(r1) */ 52 | "stfd 15,-136(1)\n" /* included and 0(r1) excluded */ 53 | "stfd 16,-128(1)\n" 54 | "stfd 17,-120(1)\n" 55 | "stfd 18,-112(1)\n" 56 | "stfd 19,-104(1)\n" 57 | "stfd 20,-96(1)\n" 58 | "stfd 21,-88(1)\n" 59 | "stfd 22,-80(1)\n" 60 | "stfd 23,-72(1)\n" 61 | "stfd 24,-64(1)\n" 62 | "stfd 25,-56(1)\n" 63 | "stfd 26,-48(1)\n" 64 | "stfd 27,-40(1)\n" 65 | "stfd 28,-32(1)\n" 66 | "stfd 29,-24(1)\n" 67 | "stfd 30,-16(1)\n" 68 | "stfd 31,-8(1)\n" 69 | 70 | "li 12,-480\n" /* the VR save area is between -480(r1) */ 71 | "stvx 20,12,1\n" /* included and -288(r1) excluded */ 72 | "li 12,-464\n" 73 | "stvx 21,12,1\n" 74 | "li 12,-448\n" 75 | "stvx 22,12,1\n" 76 | "li 12,-432\n" 77 | "stvx 23,12,1\n" 78 | "li 12,-416\n" 79 | "stvx 24,12,1\n" 80 | "li 12,-400\n" 81 | "stvx 25,12,1\n" 82 | "li 12,-384\n" 83 | "stvx 26,12,1\n" 84 | "li 12,-368\n" 85 | "stvx 27,12,1\n" 86 | "li 12,-352\n" 87 | "stvx 28,12,1\n" 88 | "li 12,-336\n" 89 | "stvx 29,12,1\n" 90 | "li 12,-320\n" 91 | "stvx 30,12,1\n" 92 | "li 12,-304\n" 93 | "stvx 31,12,1\n" 94 | 95 | "stdu 1,-528(1)\n" /* Create stack frame */ 96 | 97 | "std 2, "TOC_AREA"(1)\n" /* Save TOC in the "TOC save area"*/ 98 | "mfcr 12\n" /* Save CR in the "CR save area" */ 99 | "std 12, 8(1)\n" 100 | 101 | "mr 14, %[restore_state]\n" /* save 'restore_state' for later */ 102 | "mr 15, %[extra]\n" /* save 'extra' for later */ 103 | "mr 12, %[save_state]\n" /* move 'save_state' into r12 for branching */ 104 | "mr 3, 1\n" /* arg 1: current (old) stack pointer */ 105 | "mr 4, 15\n" /* arg 2: extra */ 106 | 107 | "stdu 1, -48(1)\n" /* create temp stack space (see below) */ 108 | #ifdef __BIG_ENDIAN__ 109 | "ld 0, 0(12)\n" 110 | "ld 11, 16(12)\n" 111 | "mtctr 0\n" 112 | "ld 2, 8(12)\n" 113 | #else 114 | "mtctr 12\n" /* r12 is fixed by this ABI */ 115 | #endif 116 | "bctrl\n" /* call save_state() */ 117 | "addi 1, 1, 48\n" /* destroy temp stack space */ 118 | 119 | "cmpdi 3, 0\n" /* skip the rest if the return value is null */ 120 | "bt eq, zero\n" 121 | 122 | "mr 1, 3\n" /* change the stack pointer */ 123 | /* From now on, the stack pointer is modified, but the content of the 124 | stack is not restored yet. It contains only garbage here. */ 125 | 126 | "mr 4, 15\n" /* arg 2: extra */ 127 | /* arg 1: current (new) stack pointer 128 | is already in r3 */ 129 | 130 | "stdu 1, -48(1)\n" /* create temp stack space for callee to use */ 131 | /* ^^^ we have to be careful. The function call will store the link 132 | register in the current frame (as the ABI) dictates. But it will 133 | then trample it with the restore! We fix this by creating a fake 134 | stack frame */ 135 | 136 | #ifdef __BIG_ENDIAN__ 137 | "ld 0, 0(14)\n" /* 'restore_state' is in r14 */ 138 | "ld 11, 16(14)\n" 139 | "mtctr 0\n" 140 | "ld 2, 8(14)\n" 141 | #endif 142 | #ifdef __LITTLE_ENDIAN__ 143 | "mr 12, 14\n" /* copy 'restore_state' */ 144 | "mtctr 12\n" /* r12 is fixed by this ABI */ 145 | #endif 146 | 147 | "bctrl\n" /* call restore_state() */ 148 | "addi 1, 1, 48\n" /* destroy temp stack space */ 149 | 150 | /* The stack's content is now restored. */ 151 | 152 | "zero:\n" 153 | 154 | /* Epilogue */ 155 | 156 | "ld 2, "TOC_AREA"(1)\n" /* restore the TOC */ 157 | "ld 12,8(1)\n" /* restore the condition register */ 158 | "mtcrf 0xff, 12\n" 159 | 160 | "addi 1,1,528\n" /* restore stack pointer */ 161 | 162 | "li 12,-480\n" /* restore vector registers */ 163 | "lvx 20,12,1\n" 164 | "li 12,-464\n" 165 | "lvx 21,12,1\n" 166 | "li 12,-448\n" 167 | "lvx 22,12,1\n" 168 | "li 12,-432\n" 169 | "lvx 23,12,1\n" 170 | "li 12,-416\n" 171 | "lvx 24,12,1\n" 172 | "li 12,-400\n" 173 | "lvx 25,12,1\n" 174 | "li 12,-384\n" 175 | "lvx 26,12,1\n" 176 | "li 12,-368\n" 177 | "lvx 27,12,1\n" 178 | "li 12,-352\n" 179 | "lvx 28,12,1\n" 180 | "li 12,-336\n" 181 | "lvx 29,12,1\n" 182 | "li 12,-320\n" 183 | "lvx 30,12,1\n" 184 | "li 12,-304\n" 185 | "lvx 31,12,1\n" 186 | 187 | "ld 14,-288(1)\n" /* restore general purpose registers */ 188 | "ld 15,-280(1)\n" 189 | "ld 16,-272(1)\n" 190 | "ld 17,-264(1)\n" 191 | "ld 18,-256(1)\n" 192 | "ld 19,-248(1)\n" 193 | "ld 20,-240(1)\n" 194 | "ld 21,-232(1)\n" 195 | "ld 22,-224(1)\n" 196 | "ld 23,-216(1)\n" 197 | "ld 24,-208(1)\n" 198 | "ld 25,-200(1)\n" 199 | "ld 26,-192(1)\n" 200 | "ld 27,-184(1)\n" 201 | "ld 28,-176(1)\n" 202 | "ld 29,-168(1)\n" 203 | "ld 30,-160(1)\n" 204 | "ld 31,-152(1)\n" 205 | 206 | "lfd 14,-144(1)\n" /* restore floating point registers */ 207 | "lfd 15,-136(1)\n" 208 | "lfd 16,-128(1)\n" 209 | "lfd 17,-120(1)\n" 210 | "lfd 18,-112(1)\n" 211 | "lfd 19,-104(1)\n" 212 | "lfd 20,-96(1)\n" 213 | "lfd 21,-88(1)\n" 214 | "lfd 22,-80(1)\n" 215 | "lfd 23,-72(1)\n" 216 | "lfd 24,-64(1)\n" 217 | "lfd 25,-56(1)\n" 218 | "lfd 26,-48(1)\n" 219 | "lfd 27,-40(1)\n" 220 | "lfd 28,-32(1)\n" 221 | "ld 0, 16(1)\n" 222 | "lfd 29,-24(1)\n" 223 | "mtlr 0\n" 224 | "lfd 30,-16(1)\n" 225 | "lfd 31,-8(1)\n" 226 | 227 | : "=r"(result) /* output variable: expected to be r3 */ 228 | : [restore_state]"r"(restore_state), /* input variables */ 229 | [save_state]"r"(save_state), 230 | [extra]"r"(extra) 231 | ); 232 | return result; 233 | } 234 | -------------------------------------------------------------------------------- /src/switch_s390x_gcc.h: -------------------------------------------------------------------------------- 1 | /* This depends on these attributes so that gcc generates a function 2 | with no code before the asm, and only "blr" after. */ 3 | static __attribute__((noinline, optimize("O2"))) 4 | void *slp_switch(void *(*save_state)(void*, void*), 5 | void *(*restore_state)(void*, void*), 6 | void *extra) 7 | { 8 | void *result; 9 | __asm__ volatile ( 10 | /* The Stackless version by Kristjan Valur Jonsson, 11 | ported to s390x by Richard Plangger */ 12 | 13 | "stmg 6,15,48(15)\n" 14 | 15 | // store f8 - f15 into the stack frame that is not used! 16 | "std 8,128(15)\n" 17 | "std 9,136(15)\n" 18 | "std 10,144(15)\n" 19 | "std 11,152(15)\n" 20 | 21 | "std 12,16(15)\n" 22 | "std 13,24(15)\n" 23 | "std 14,32(15)\n" 24 | "std 15,40(15)\n" 25 | 26 | "lgr 10, %[restore_state]\n" /* save 'restore_state' for later */ 27 | "lgr 11, %[extra]\n" /* save 'extra' for later */ 28 | "lgr 14, %[save_state]\n" /* move 'save_state' into r14 for branching */ 29 | "lgr 2, 15\n" /* arg 1: current (old) stack pointer */ 30 | "lgr 3, 11\n" /* arg 2: extra */ 31 | 32 | "lay 15,-160(15)\n" /* create stack frame */ 33 | "basr 14, 14\n" /* call save_state() */ 34 | "lay 15,160(15)\n" 35 | 36 | "cgij 2, 0, 8, zero\n" /* skip the rest if the return value is null */ 37 | 38 | "lgr 15, 2\n" /* change the stack pointer */ 39 | 40 | /* From now on, the stack pointer is modified, but the content of the 41 | stack is not restored yet. It contains only garbage here. */ 42 | /* arg 1: current (new) stack pointer 43 | is already in r2 */ 44 | "lgr 3, 11\n" /* arg 2: extra */ 45 | 46 | "lay 15,-160(15)\n" /* create stack frame */ 47 | "basr 14, 10\n" /* call restore_state() */ 48 | "lay 15,160(15)\n" 49 | 50 | /* The stack's content is now restored. */ 51 | 52 | "zero:\n" 53 | 54 | /* Epilogue */ 55 | "ld 8,128(15)\n" 56 | "ld 9,136(15)\n" 57 | "ld 10,144(15)\n" 58 | "ld 11,152(15)\n" 59 | 60 | "ld 12,16(15)\n" 61 | "ld 13,24(15)\n" 62 | "ld 14,32(15)\n" 63 | "ld 15,40(15)\n" 64 | 65 | "lmg 6,15,48(15)\n" 66 | 67 | : "=r"(result) /* output variable: expected to be r2 */ 68 | : [restore_state]"r"(restore_state), /* input variables */ 69 | [save_state]"r"(save_state), 70 | [extra]"r"(extra) 71 | ); 72 | return result; 73 | } 74 | -------------------------------------------------------------------------------- /src/switch_x64_msvc.asm: -------------------------------------------------------------------------------- 1 | ; 2 | ; stack switching code for MASM on x64 3 | ; Kristjan Valur Jonsson, apr 2011 4 | ; 5 | 6 | include macamd64.inc 7 | 8 | pop_reg MACRO reg 9 | pop reg 10 | ENDM 11 | 12 | load_xmm128 macro Reg, Offset 13 | movdqa Reg, Offset[rsp] 14 | endm 15 | 16 | .code 17 | 18 | ;arguments save_state, restore_state, extra are passed in rcx, rdx, r8 respectively 19 | ;slp_switch PROC FRAME 20 | NESTED_ENTRY slp_switch, _TEXT$00 21 | ; save all registers that the x64 ABI specifies as non-volatile. 22 | ; This includes some mmx registers. May not always be necessary, 23 | ; unless our application is doing 3D, but better safe than sorry. 24 | alloc_stack 168; 10 * 16 bytes, plus 8 bytes to make stack 16 byte aligned 25 | save_xmm128 xmm15, 144 26 | save_xmm128 xmm14, 128 27 | save_xmm128 xmm13, 112 28 | save_xmm128 xmm12, 96 29 | save_xmm128 xmm11, 80 30 | save_xmm128 xmm10, 64 31 | save_xmm128 xmm9, 48 32 | save_xmm128 xmm8, 32 33 | save_xmm128 xmm7, 16 34 | save_xmm128 xmm6, 0 35 | 36 | push_reg r15 37 | push_reg r14 38 | push_reg r13 39 | push_reg r12 40 | 41 | push_reg rbp 42 | push_reg rbx 43 | push_reg rdi 44 | push_reg rsi 45 | 46 | sub rsp, 20h ;allocate shadow stack space for the arguments (must be multiple of 16) 47 | .allocstack 20h 48 | .endprolog 49 | 50 | ;save argments in nonvolatile registers 51 | mov r12, rcx ;save_state 52 | mov r13, rdx 53 | mov r14, r8 54 | 55 | ; load stack base that we are saving minus the callee argument 56 | ; shadow stack. We don't want that clobbered 57 | lea rcx, [rsp+20h] 58 | mov rdx, r14 59 | call r12 ;pass stackpointer, return new stack pointer in eax 60 | 61 | ; an null value means that we don't restore. 62 | test rax, rax 63 | jz exit 64 | 65 | ;actual stack switch (and re-allocating the shadow stack): 66 | lea rsp, [rax-20h] 67 | 68 | mov rcx, rax ;pass new stack pointer 69 | mov rdx, r14 70 | call r13 71 | ;return the rax 72 | EXIT: 73 | 74 | add rsp, 20h 75 | pop_reg rsi 76 | pop_reg rdi 77 | pop_reg rbx 78 | pop_reg rbp 79 | 80 | pop_reg r12 81 | pop_reg r13 82 | pop_reg r14 83 | pop_reg r15 84 | 85 | load_xmm128 xmm15, 144 86 | load_xmm128 xmm14, 128 87 | load_xmm128 xmm13, 112 88 | load_xmm128 xmm12, 96 89 | load_xmm128 xmm11, 80 90 | load_xmm128 xmm10, 64 91 | load_xmm128 xmm9, 48 92 | load_xmm128 xmm8, 32 93 | load_xmm128 xmm7, 16 94 | load_xmm128 xmm6, 0 95 | add rsp, 168 96 | ret 97 | 98 | NESTED_END slp_switch, _TEXT$00 99 | ;slp_switch ENDP 100 | 101 | END -------------------------------------------------------------------------------- /src/switch_x64_msvc.h: -------------------------------------------------------------------------------- 1 | /* The actual stack saving function, which just stores the stack, 2 | * this declared in an .asm file 3 | */ 4 | extern void *slp_switch(void *(*save_state)(void*, void*), 5 | void *(*restore_state)(void*, void*), 6 | void *extra); 7 | 8 | #undef STATIC_NOINLINE 9 | #define STATIC_NOINLINE static __declspec(noinline) 10 | -------------------------------------------------------------------------------- /src/switch_x86_64_gcc.h: -------------------------------------------------------------------------------- 1 | 2 | static void *slp_switch(void *(*save_state)(void*, void*), 3 | void *(*restore_state)(void*, void*), 4 | void *extra) 5 | { 6 | void *result, *garbage1, *garbage2; 7 | __asm__ volatile ( 8 | "pushq %%rbp\n" 9 | "pushq %%rbx\n" /* push the registers specified as caller-save */ 10 | "pushq %%r12\n" 11 | "pushq %%r13\n" 12 | "pushq %%r14\n" 13 | "movq %%rsp, %%rbp\n" 14 | "andq $-16, %%rsp\n" /* <= align the stack here... */ 15 | "pushq %%r15\n" 16 | "pushq %%rbp\n" /* ...so that rsp is now a multiple of 16 */ 17 | 18 | "movq %%rax, %%r12\n" /* save 'restore_state' for later */ 19 | "movq %%rsi, %%r13\n" /* save 'extra' for later */ 20 | 21 | /* arg 2: extra (already in rsi) */ 22 | "movq %%rsp, %%rdi\n" /* arg 1: current (old) stack pointer */ 23 | "call *%%rcx\n" /* call save_state() */ 24 | 25 | "testq %%rax, %%rax\n" /* skip the rest if the return value is null */ 26 | "jz 0f\n" 27 | 28 | "movq %%rax, %%rsp\n" /* change the stack pointer */ 29 | 30 | /* From now on, the stack pointer is modified, but the content of the 31 | stack is not restored yet. It contains only garbage here. */ 32 | 33 | "movq %%r13, %%rsi\n" /* arg 2: extra */ 34 | "movq %%rax, %%rdi\n" /* arg 1: current (new) stack pointer */ 35 | "call *%%r12\n" /* call restore_state() */ 36 | 37 | /* The stack's content is now restored. */ 38 | 39 | "0:\n" 40 | "popq %%rbp\n" 41 | "popq %%r15\n" 42 | "movq %%rbp, %%rsp\n" 43 | "popq %%r14\n" 44 | "popq %%r13\n" 45 | "popq %%r12\n" 46 | "popq %%rbx\n" 47 | "popq %%rbp\n" 48 | 49 | : "=a"(result), /* output variables */ 50 | "=c"(garbage1), 51 | "=S"(garbage2) 52 | : "a"(restore_state), /* input variables */ 53 | "c"(save_state), 54 | "S"(extra) 55 | : "memory", "rdx", "rdi", "r8", "r9", "r10", "r11", 56 | "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", 57 | "xmm8", "xmm9", "xmm10","xmm11","xmm12","xmm13","xmm14","xmm15" 58 | ); 59 | return result; 60 | } 61 | -------------------------------------------------------------------------------- /src/switch_x86_gcc.h: -------------------------------------------------------------------------------- 1 | 2 | static void *slp_switch(void *(*save_state)(void*, void*), 3 | void *(*restore_state)(void*, void*), 4 | void *extra) 5 | { 6 | void *result, *garbage1, *garbage2; 7 | __asm__ volatile ( 8 | "pushl %%ebp\n" 9 | "pushl %%ebx\n" /* push some registers that may contain */ 10 | "pushl %%esi\n" /* some value that is meant to be saved */ 11 | "movl %%esp, %%ebp\n" 12 | "andl $-16, %%esp\n" /* <= align the stack here, for the calls */ 13 | "pushl %%edi\n" 14 | "pushl %%ebp\n" 15 | 16 | "movl %%eax, %%esi\n" /* save 'restore_state' for later */ 17 | "movl %%edx, %%edi\n" /* save 'extra' for later */ 18 | 19 | "movl %%esp, %%eax\n" 20 | 21 | "pushl %%edx\n" /* arg 2: extra */ 22 | "pushl %%eax\n" /* arg 1: current (old) stack pointer */ 23 | "call *%%ecx\n" /* call save_state() */ 24 | 25 | "testl %%eax, %%eax\n"/* skip the rest if the return value is null */ 26 | "jz 0f\n" 27 | 28 | "movl %%eax, %%esp\n" /* change the stack pointer */ 29 | 30 | /* From now on, the stack pointer is modified, but the content of the 31 | stack is not restored yet. It contains only garbage here. */ 32 | 33 | "pushl %%edi\n" /* arg 2: extra */ 34 | "pushl %%eax\n" /* arg 1: current (new) stack pointer */ 35 | "call *%%esi\n" /* call restore_state() */ 36 | 37 | /* The stack's content is now restored. */ 38 | 39 | "0:\n" 40 | "addl $8, %%esp\n" 41 | "popl %%ebp\n" 42 | "popl %%edi\n" 43 | "movl %%ebp, %%esp\n" 44 | "popl %%esi\n" 45 | "popl %%ebx\n" 46 | "popl %%ebp\n" 47 | 48 | : "=a"(result), /* output variables */ 49 | "=c"(garbage1), 50 | "=d"(garbage2) 51 | : "a"(restore_state), /* input variables */ 52 | "c"(save_state), 53 | "d"(extra) 54 | : "memory" 55 | ); 56 | /* Note: we should also list all fp/xmm registers, but is there a way 57 | to list only the ones used by the current compilation target? 58 | For now we will just ignore the issue and hope (reasonably) that 59 | this function is never inlined all the way into 3rd-party user code. */ 60 | return result; 61 | } 62 | -------------------------------------------------------------------------------- /src/switch_x86_msvc.asm: -------------------------------------------------------------------------------- 1 | 2 | .386 3 | .model flat, c 4 | 5 | .code 6 | 7 | slp_switch_raw PROC save_state:DWORD, restore_state:DWORD, extra:DWORD 8 | 9 | ;save registers. EAX ECX and EDX are available for function use and thus 10 | ;do not have to be stored. 11 | push ebx 12 | push esi 13 | push edi 14 | push ebp 15 | 16 | mov esi, restore_state ; /* save 'restore_state' for later */ 17 | mov edi, extra ; /* save 'extra' for later */ 18 | 19 | mov eax, esp 20 | 21 | push edi ; /* arg 2: extra */ 22 | push eax ; /* arg 1: current (old) stack pointer */ 23 | mov ecx, save_state 24 | call ecx ; /* call save_state() */ 25 | 26 | test eax, eax; /* skip the restore if the return value is null */ 27 | jz exit 28 | 29 | mov esp, eax; /* change the stack pointer */ 30 | 31 | push edi ; /* arg 2: extra */ 32 | push eax ; /* arg 1: current (new) stack pointer */ 33 | call esi ; /* call restore_state() */ 34 | 35 | exit: 36 | add esp, 8 37 | pop ebp 38 | pop edi 39 | pop esi 40 | pop ebx 41 | ret 42 | slp_switch_raw ENDP 43 | 44 | end -------------------------------------------------------------------------------- /src/switch_x86_msvc.h: -------------------------------------------------------------------------------- 1 | /* The actual stack saving function, which just stores the stack, 2 | * this declared in an .asm file 3 | */ 4 | extern void *slp_switch_raw(void *(*save_state)(void*, void*), 5 | void *(*restore_state)(void*, void*), 6 | void *extra); 7 | 8 | #undef STATIC_NOINLINE 9 | #define STATIC_NOINLINE static __declspec(noinline) 10 | 11 | #define WIN32_LEAN_AND_MEAN 12 | #include 13 | 14 | /* Store any other runtime information on the local stack */ 15 | #pragma optimize("", off) /* so that autos are stored on the stack */ 16 | #pragma warning(disable:4733) /* disable warning about modifying FS[0] */ 17 | 18 | static void *slp_switch(void *(*save_state)(void*, void*), 19 | void *(*restore_state)(void*, void*), 20 | void *extra) 21 | { 22 | /* store the structured exception state for this stack */ 23 | DWORD seh_state = __readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); 24 | void * result = slp_switch_raw(save_state, restore_state, extra); 25 | __writefsdword(FIELD_OFFSET(NT_TIB, ExceptionList), seh_state); 26 | return result; 27 | } 28 | #pragma warning(default:4733) /* disable warning about modifying FS[0] */ 29 | #pragma optimize("", on) 30 | -------------------------------------------------------------------------------- /tests/test_fibers.py: -------------------------------------------------------------------------------- 1 | 2 | import copy 3 | import time 4 | import threading 5 | import unittest 6 | 7 | import os 8 | import sys 9 | import traceback 10 | 11 | import fibers 12 | from fibers import Fiber, current 13 | import pytest 14 | 15 | 16 | is_pypy = hasattr(sys, 'pypy_version_info') 17 | 18 | 19 | class SomeError(Exception): 20 | pass 21 | 22 | 23 | def fmain(seen): 24 | try: 25 | current().parent.switch() 26 | except: 27 | seen.append(sys.exc_info()[0]) 28 | raise 29 | raise SomeError 30 | 31 | 32 | def send_exception(g, exc): 33 | # note: send_exception(g, exc) can be now done with g.throw(exc). 34 | # the purpose of this test is to explicitly check the propagation rules. 35 | def crasher(exc): 36 | raise exc 37 | g1 = Fiber(target=crasher, args=(exc, ), parent=g) 38 | g1.switch() 39 | 40 | 41 | class FiberTests(unittest.TestCase): 42 | 43 | def test_simple(self): 44 | lst = [] 45 | 46 | def f(): 47 | lst.append(1) 48 | current().parent.switch() 49 | lst.append(3) 50 | g = Fiber(f) 51 | lst.append(0) 52 | g.switch() 53 | lst.append(2) 54 | g.switch() 55 | lst.append(4) 56 | assert lst == list(range(5)) 57 | 58 | def test_simple2(self): 59 | lst = [] 60 | 61 | def f(): 62 | lst.append(1) 63 | current().parent.switch() 64 | lst.append(3) 65 | g = Fiber(f) 66 | lst.append(0) 67 | g.switch() 68 | lst.append(2) 69 | g.switch() 70 | lst.append(4) 71 | assert lst == list(range(5)) 72 | 73 | def test_clean_callstack(self): 74 | lst = [] 75 | 76 | def f(): 77 | for line in traceback.format_stack(): 78 | lst.append(line) 79 | f() 80 | expected = [lst[-1]] 81 | lst = [] 82 | g = Fiber(f) 83 | g.switch() 84 | assert lst == expected 85 | 86 | def test_two_children(self): 87 | lst = [] 88 | 89 | def f(): 90 | lst.append(1) 91 | current().parent.switch() 92 | lst.extend([1, 1]) 93 | g = Fiber(f) 94 | h = Fiber(f) 95 | g.switch() 96 | assert len(lst) == 1 97 | h.switch() 98 | assert len(lst) == 2 99 | h.switch() 100 | assert len(lst) == 4 101 | assert h.is_alive() == False 102 | g.switch() 103 | assert len(lst) == 6 104 | assert g.is_alive() == False 105 | 106 | def test_two_recursive_children(self): 107 | lst = [] 108 | 109 | def f(): 110 | lst.append(1) 111 | current().parent.switch() 112 | 113 | def h(): 114 | lst.append(1) 115 | i = Fiber(f) 116 | i.switch() 117 | lst.append(1) 118 | g = Fiber(h) 119 | g.switch() 120 | assert len(lst) == 3 121 | 122 | def test_exception(self): 123 | seen = [] 124 | g1 = Fiber(target=fmain, args=(seen, )) 125 | g2 = Fiber(target=fmain, args=(seen, )) 126 | g1.switch() 127 | g2.switch() 128 | g2.parent = g1 129 | assert seen == [] 130 | with pytest.raises(SomeError): 131 | g2.switch() 132 | assert seen == [SomeError] 133 | 134 | def test_send_exception(self): 135 | seen = [] 136 | g1 = Fiber(target=fmain, args=(seen, )) 137 | g1.switch() 138 | with pytest.raises(KeyError): 139 | send_exception(g1, KeyError) 140 | assert seen == [KeyError] 141 | 142 | # def test_frame(self): 143 | # def f1(): 144 | # f = sys._getframe(0) 145 | # self.assertEqual(f.f_back, None) 146 | # current().parent.switch(f) 147 | # return "meaning of life" 148 | # g = Fiber(f1) 149 | # frame = g.switch() 150 | # self.assertTrue(frame is g.t_frame) 151 | # self.assertTrue(g) 152 | # next = g.switch() 153 | # self.assertFalse(g.is_alive()) 154 | # self.assertEqual(next, 'meaning of life') 155 | # self.assertEqual(g.t_frame, None) 156 | 157 | def test_threads(self): 158 | success = [] 159 | 160 | def f(): 161 | self.test_simple2() 162 | success.append(True) 163 | ths = [threading.Thread(target=f) for i in range(10)] 164 | for th in ths: 165 | th.start() 166 | for th in ths: 167 | th.join() 168 | assert len(success) == len(ths) 169 | 170 | def test_thread_bug(self): 171 | def runner(x): 172 | g = Fiber(lambda: time.sleep(x)) 173 | g.switch() 174 | t1 = threading.Thread(target=runner, args=(0.2,)) 175 | t2 = threading.Thread(target=runner, args=(0.3,)) 176 | t1.start() 177 | t2.start() 178 | t1.join() 179 | t2.join() 180 | 181 | def test_switch_after_thread_of_prev_fiber_exited(self): 182 | def thread1(): 183 | def fiber1(): 184 | time.sleep(0.4) 185 | f11 = fibers.Fiber(fiber1) 186 | f11.switch() 187 | def thread2(): 188 | time.sleep(0.2) 189 | def fiber1(): 190 | time.sleep(0.4) 191 | f22.switch() 192 | def fiber2(): 193 | pass 194 | f21 = fibers.Fiber(fiber1) 195 | f22 = fibers.Fiber(fiber2) 196 | f21.switch() 197 | t1 = threading.Thread(target=thread1) 198 | t2 = threading.Thread(target=thread2) 199 | t1.start() 200 | t2.start() 201 | t1.join() 202 | t2.join() 203 | 204 | def test_switch_to_another_thread(self): 205 | data = {} 206 | error = None 207 | created_event = threading.Event() 208 | done_event = threading.Event() 209 | 210 | def foo(): 211 | data['g'] = Fiber(lambda: None) 212 | created_event.set() 213 | done_event.wait() 214 | thread = threading.Thread(target=foo) 215 | thread.start() 216 | created_event.wait() 217 | try: 218 | data['g'].switch() 219 | except fibers.error: 220 | error = sys.exc_info()[1] 221 | assert error != None, "fibers.error was not raised!" 222 | done_event.set() 223 | thread.join() 224 | 225 | def test_threaded_reparent(self): 226 | data = {} 227 | created_event = threading.Event() 228 | done_event = threading.Event() 229 | 230 | def foo(): 231 | data['g'] = Fiber(lambda: None) 232 | created_event.set() 233 | done_event.wait() 234 | 235 | def blank(): 236 | current().parent.switch() 237 | 238 | def setparent(g, value): 239 | g.parent = value 240 | 241 | thread = threading.Thread(target=foo) 242 | thread.start() 243 | created_event.wait() 244 | g = Fiber(blank) 245 | g.switch() 246 | with pytest.raises(ValueError): 247 | setparent(g, data['g']) 248 | done_event.set() 249 | thread.join() 250 | 251 | def test_throw_doesnt_crash(self): 252 | result = [] 253 | def worker(): 254 | current().parent.switch() 255 | def creator(): 256 | g = Fiber(worker) 257 | g.switch() 258 | result.append(g) 259 | t = threading.Thread(target=creator) 260 | t.start() 261 | t.join() 262 | with pytest.raises(fibers.error): 263 | result[0].throw(SomeError()) 264 | 265 | def test_threaded_updatecurrent(self): 266 | # FIXME (hangs?) 267 | return 268 | # released when main thread should execute 269 | lock1 = threading.Lock() 270 | lock1.acquire() 271 | # released when another thread should execute 272 | lock2 = threading.Lock() 273 | lock2.acquire() 274 | class finalized(object): 275 | def __del__(self): 276 | # happens while in green_updatecurrent() in main greenlet 277 | # should be very careful not to accidentally call it again 278 | # at the same time we must make sure another thread executes 279 | lock2.release() 280 | lock1.acquire() 281 | # now ts_current belongs to another thread 282 | def deallocator(): 283 | current().parent.switch() 284 | def fthread(): 285 | lock2.acquire() 286 | current() 287 | del g[0] 288 | lock1.release() 289 | lock2.acquire() 290 | current() 291 | lock1.release() 292 | main = current() 293 | g = [Fiber(deallocator)] 294 | g[0].bomb = finalized() 295 | g[0].switch() 296 | t = threading.Thread(target=fthread) 297 | t.start() 298 | # let another thread grab ts_current and deallocate g[0] 299 | lock2.release() 300 | lock1.acquire() 301 | # this is the corner stone 302 | # getcurrent() will notice that ts_current belongs to another thread 303 | # and start the update process, which would notice that g[0] should 304 | # be deallocated, and that will execute an object's finalizer. Now, 305 | # that object will let another thread run so it can grab ts_current 306 | # again, which would likely crash the interpreter if there's no 307 | # check for this case at the end of green_updatecurrent(). This test 308 | # passes if getcurrent() returns correct result, but it's likely 309 | # to randomly crash if it's not anyway. 310 | assert current() == main 311 | # wait for another thread to complete, just in case 312 | t.join() 313 | 314 | def test_exc_state(self): 315 | def f(): 316 | try: 317 | raise ValueError('fun') 318 | except: 319 | exc_info = sys.exc_info() 320 | t = Fiber(h) 321 | t.switch() 322 | assert exc_info == sys.exc_info() 323 | del t 324 | 325 | def h(): 326 | assert sys.exc_info() == (None, None, None) 327 | 328 | g = Fiber(f) 329 | g.switch() 330 | 331 | def test_instance_dict(self): 332 | if is_pypy: 333 | return 334 | def f(): 335 | current().test = 42 336 | def deldict(g): 337 | del g.__dict__ 338 | def setdict(g, value): 339 | g.__dict__ = value 340 | g = Fiber(f) 341 | assert g.__dict__ == {} 342 | g.switch() 343 | assert g.test == 42 344 | assert g.__dict__ == {'test': 42} 345 | g.__dict__ = g.__dict__ 346 | assert g.__dict__ == {'test': 42} 347 | with pytest.raises(AttributeError): 348 | deldict(g) 349 | with pytest.raises(TypeError): 350 | setdict(g, 42) 351 | 352 | def test_deepcopy(self): 353 | with pytest.raises(TypeError): 354 | copy.copy(Fiber()) 355 | with pytest.raises(TypeError): 356 | copy.deepcopy(Fiber()) 357 | 358 | def test_finished_parent(self): 359 | def f(): 360 | return 42 361 | g = Fiber(f) 362 | g.switch() 363 | assert not g.is_alive() 364 | with pytest.raises(ValueError): 365 | Fiber(parent=g) 366 | 367 | 368 | if __name__ == '__main__': 369 | unittest.main(verbosity=2) 370 | 371 | -------------------------------------------------------------------------------- /tests/test_gc.py: -------------------------------------------------------------------------------- 1 | 2 | import gc 3 | import unittest 4 | import weakref 5 | 6 | import os 7 | import sys 8 | 9 | from fibers import Fiber, current 10 | 11 | 12 | class GCTests(unittest.TestCase): 13 | 14 | def test_circular_fiber(self): 15 | class circular_fiber(Fiber): 16 | pass 17 | o = circular_fiber() 18 | o.self = o 19 | o = weakref.ref(o) 20 | gc.collect() 21 | assert o() is None 22 | assert not gc.garbage, gc.garbage 23 | 24 | def test_dead_circular_ref(self): 25 | o = weakref.ref(Fiber(current).switch()) 26 | gc.collect() 27 | assert o() is None 28 | assert not gc.garbage, gc.garbage 29 | 30 | def test_inactive_ref(self): 31 | o = Fiber(lambda: None) 32 | o = weakref.ref(o) 33 | gc.collect() 34 | assert o() is None 35 | assert not gc.garbage, gc.garbage 36 | 37 | def test_finalizer_crash(self): 38 | # This test is designed to crash when active greenlets 39 | # are made garbage collectable, until the underlying 40 | # problem is resolved. How does it work: 41 | # - order of object creation is important 42 | # - array is created first, so it is moved to unreachable first 43 | # - we create a cycle between a greenlet and this array 44 | # - we create an object that participates in gc, is only 45 | # referenced by a greenlet, and would corrupt gc lists 46 | # on destruction, the easiest is to use an object with 47 | # a finalizer 48 | # - because array is the first object in unreachable it is 49 | # cleared first, which causes all references to greenlet 50 | # to disappear and causes greenlet to be destroyed, but since 51 | # it is still live it causes a switch during gc, which causes 52 | # an object with finalizer to be destroyed, which causes stack 53 | # corruption and then a crash 54 | class object_with_finalizer(object): 55 | def __del__(self): 56 | pass 57 | array = [] 58 | parent = current() 59 | def greenlet_body(): 60 | current().object = object_with_finalizer() 61 | try: 62 | parent.switch() 63 | finally: 64 | del current().object 65 | g = Fiber(greenlet_body) 66 | g.array = array 67 | array.append(g) 68 | g.switch() 69 | del array 70 | del g 71 | current() 72 | gc.collect() 73 | 74 | 75 | if __name__ == '__main__': 76 | unittest.main(verbosity=2) 77 | 78 | -------------------------------------------------------------------------------- /tests/test_generator.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | import os 5 | import sys 6 | 7 | import fibers 8 | from fibers import Fiber 9 | 10 | 11 | class genlet(Fiber): 12 | 13 | def __init__(self, *args, **kwds): 14 | self.args = args 15 | self.kwds = kwds 16 | Fiber.__init__(self, target=self.run) 17 | 18 | def run(self): 19 | fn, = self.fn 20 | fn(*self.args, **self.kwds) 21 | 22 | def __iter__(self): 23 | return self 24 | 25 | def __next__(self): 26 | self.parent = fibers.current() 27 | result = self.switch() 28 | if self.is_alive(): 29 | return result 30 | else: 31 | raise StopIteration 32 | 33 | # Hack: Python < 2.6 compatibility 34 | next = __next__ 35 | 36 | 37 | def Yield(value): 38 | g = fibers.current() 39 | while not isinstance(g, genlet): 40 | if g is None: 41 | raise RuntimeError('yield outside a genlet') 42 | g = g.parent 43 | g.parent.switch(value) 44 | 45 | 46 | def generator(func): 47 | class generator(genlet): 48 | fn = (func,) 49 | return generator 50 | 51 | # ____________________________________________________________ 52 | 53 | 54 | class GeneratorTests(unittest.TestCase): 55 | def test_generator(self): 56 | seen = [] 57 | 58 | def g(n): 59 | for i in range(n): 60 | seen.append(i) 61 | Yield(i) 62 | g = generator(g) 63 | for k in range(3): 64 | for j in g(5): 65 | seen.append(j) 66 | assert seen == 3 * [0, 0, 1, 1, 2, 2, 3, 3, 4, 4] 67 | 68 | 69 | if __name__ == '__main__': 70 | unittest.main(verbosity=2) 71 | 72 | -------------------------------------------------------------------------------- /tests/test_generator_nested.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | import os 5 | import sys 6 | 7 | import fibers 8 | from fibers import Fiber 9 | 10 | 11 | class genlet(Fiber): 12 | 13 | def __init__(self, *args, **kwds): 14 | self.args = args 15 | self.kwds = kwds 16 | self.child = None 17 | Fiber.__init__(self, target=self.run) 18 | 19 | def run(self): 20 | fn, = self.fn 21 | fn(*self.args, **self.kwds) 22 | 23 | def __iter__(self): 24 | return self 25 | 26 | def set_child(self, child): 27 | self.child = child 28 | 29 | def __next__(self): 30 | if self.child: 31 | child = self.child 32 | while child.child: 33 | tmp = child 34 | child = child.child 35 | tmp.child = None 36 | 37 | result = child.switch() 38 | else: 39 | self.parent = fibers.current() 40 | result = self.switch() 41 | 42 | if self.is_alive(): 43 | return result 44 | else: 45 | raise StopIteration 46 | 47 | # Hack: Python < 2.6 compatibility 48 | next = __next__ 49 | 50 | 51 | def Yield(value, level=1): 52 | g = fibers.current() 53 | 54 | while level != 0: 55 | if not isinstance(g, genlet): 56 | raise RuntimeError('yield outside a genlet') 57 | if level > 1: 58 | g.parent.set_child(g) 59 | g = g.parent 60 | level -= 1 61 | 62 | g.switch(value) 63 | 64 | 65 | def Genlet(func): 66 | class Genlet(genlet): 67 | fn = (func,) 68 | return Genlet 69 | 70 | # ____________________________________________________________ 71 | 72 | 73 | def g1(n, seen): 74 | for i in range(n): 75 | seen.append(i + 1) 76 | yield i 77 | 78 | 79 | def g2(n, seen): 80 | for i in range(n): 81 | seen.append(i + 1) 82 | Yield(i) 83 | 84 | g2 = Genlet(g2) 85 | 86 | 87 | def nested(i): 88 | Yield(i) 89 | 90 | 91 | def g3(n, seen): 92 | for i in range(n): 93 | seen.append(i + 1) 94 | nested(i) 95 | g3 = Genlet(g3) 96 | 97 | 98 | def a(n): 99 | if n == 0: 100 | return 101 | for ii in ax(n - 1): 102 | Yield(ii) 103 | Yield(n) 104 | ax = Genlet(a) 105 | 106 | 107 | def perms(l): 108 | if len(l) > 1: 109 | for e in l: 110 | # No syntactical sugar for generator expressions 111 | [Yield([e] + p) for p in perms([x for x in l if x != e])] 112 | else: 113 | Yield(l) 114 | perms = Genlet(perms) 115 | 116 | 117 | def gr1(n): 118 | for ii in range(1, n): 119 | Yield(ii) 120 | Yield(ii * ii, 2) 121 | 122 | gr1 = Genlet(gr1) 123 | 124 | 125 | def gr2(n, seen): 126 | for ii in gr1(n): 127 | seen.append(ii) 128 | 129 | gr2 = Genlet(gr2) 130 | 131 | 132 | class NestedGeneratorTests(unittest.TestCase): 133 | def test_layered_genlets(self): 134 | seen = [] 135 | for ii in gr2(5, seen): 136 | seen.append(ii) 137 | assert seen == [1, 1, 2, 4, 3, 9, 4, 16] 138 | 139 | def test_permutations(self): 140 | gen_perms = perms(list(range(4))) 141 | permutations = list(gen_perms) 142 | assert len(permutations) == 4 * 3 * 2 * 1 143 | assert [0, 1, 2, 3] in permutations 144 | assert [3, 2, 1, 0] in permutations 145 | res = [] 146 | for ii in zip(perms(list(range(4))), perms(list(range(3)))): 147 | res.append(ii) 148 | assert res == \ 149 | [([0, 1, 2, 3], [0, 1, 2]), ([0, 1, 3, 2], [0, 2, 1]), 150 | ([0, 2, 1, 3], [1, 0, 2]), ([0, 2, 3, 1], [1, 2, 0]), 151 | ([0, 3, 1, 2], [2, 0, 1]), ([0, 3, 2, 1], [2, 1, 0])] 152 | # XXX Test to make sure we are working as a generator expression 153 | 154 | def test_genlet_simple(self): 155 | for g in [g1, g2, g3]: 156 | seen = [] 157 | for k in range(3): 158 | for j in g(5, seen): 159 | seen.append(j) 160 | assert seen == 3 * [1, 0, 2, 1, 3, 2, 4, 3, 5, 4] 161 | 162 | def test_genlet_bad(self): 163 | try: 164 | Yield(10) 165 | except RuntimeError: 166 | pass 167 | 168 | def test_nested_genlets(self): 169 | seen = [] 170 | for ii in ax(5): 171 | seen.append(ii) 172 | 173 | 174 | if __name__ == '__main__': 175 | unittest.main(verbosity=2) 176 | 177 | -------------------------------------------------------------------------------- /tests/test_leaks.py: -------------------------------------------------------------------------------- 1 | 2 | import gc 3 | import threading 4 | import unittest 5 | import weakref 6 | 7 | import os 8 | import sys 9 | 10 | from fibers import Fiber, current 11 | 12 | 13 | is_pypy = hasattr(sys, 'pypy_version_info') 14 | has_refcount = hasattr(sys, 'getrefcount') 15 | 16 | 17 | class ArgRefcountTests(unittest.TestCase): 18 | 19 | def test_arg_refs(self): 20 | if not has_refcount: 21 | return 22 | args = ('a', 'b', 'c') 23 | refcount_before = sys.getrefcount(args) 24 | g = Fiber(target=lambda *x: None, args=args) 25 | assert sys.getrefcount(args) == refcount_before+1 26 | g.switch() 27 | assert sys.getrefcount(args) == refcount_before 28 | del g 29 | assert sys.getrefcount(args) == refcount_before 30 | 31 | def test_kwarg_refs(self): 32 | if not has_refcount: 33 | return 34 | kwargs = {'a': 1234} 35 | refcount_before = sys.getrefcount(kwargs) 36 | g = Fiber(lambda **x: None, kwargs=kwargs) 37 | assert sys.getrefcount(kwargs) == refcount_before+1 38 | g.switch() 39 | assert sys.getrefcount(kwargs) == refcount_before 40 | del g 41 | assert sys.getrefcount(kwargs) == refcount_before 42 | 43 | def test_threaded_leak(self): 44 | if is_pypy: 45 | return 46 | gg = [] 47 | def worker(): 48 | # only main greenlet present 49 | gg.append(weakref.ref(current())) 50 | for i in range(2): 51 | t = threading.Thread(target=worker) 52 | t.start() 53 | t.join() 54 | current() # update ts_current 55 | gc.collect() 56 | for g in gg: 57 | assert g() is None 58 | 59 | def test_threaded_adv_leak(self): 60 | if is_pypy: 61 | return 62 | gg = [] 63 | def worker(): 64 | # main and additional *finished* greenlets 65 | ll = current().ll = [] 66 | def additional(): 67 | ll.append(current()) 68 | for i in range(2): 69 | Fiber(additional).switch() 70 | gg.append(weakref.ref(current())) 71 | for i in range(2): 72 | t = threading.Thread(target=worker) 73 | t.start() 74 | t.join() 75 | current() # update ts_current 76 | gc.collect() 77 | gc.collect() 78 | for g in gg: 79 | assert g() is None 80 | 81 | 82 | if __name__ == '__main__': 83 | unittest.main(verbosity=2) 84 | 85 | -------------------------------------------------------------------------------- /tests/test_throw.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | import os 5 | import sys 6 | 7 | import fibers 8 | from fibers import Fiber 9 | import pytest 10 | 11 | 12 | def switch(val): 13 | return fibers.current().parent.switch(val) 14 | 15 | 16 | class ThrowTests(unittest.TestCase): 17 | def test_class(self): 18 | def f(): 19 | try: 20 | switch("ok") 21 | except RuntimeError: 22 | switch("ok") 23 | return 24 | switch("fail") 25 | g = Fiber(f) 26 | res = g.switch() 27 | assert res == "ok" 28 | res = g.throw(RuntimeError) 29 | assert res == "ok" 30 | 31 | def test_val(self): 32 | def f(): 33 | try: 34 | switch("ok") 35 | except RuntimeError: 36 | val = sys.exc_info()[1] 37 | if str(val) == "ciao": 38 | switch("ok") 39 | return 40 | switch("fail") 41 | 42 | g = Fiber(f) 43 | res = g.switch() 44 | assert res == "ok" 45 | res = g.throw(RuntimeError("ciao")) 46 | assert res == "ok" 47 | 48 | g = Fiber(f) 49 | res = g.switch() 50 | assert res == "ok" 51 | res = g.throw(RuntimeError, "ciao") 52 | assert res == "ok" 53 | 54 | def test_kill(self): 55 | def f(): 56 | try: 57 | switch("ok") 58 | switch("fail") 59 | except Exception as e: 60 | return e 61 | g = Fiber(f) 62 | res = g.switch() 63 | assert res == "ok" 64 | res = g.throw(ValueError) 65 | assert isinstance(res, ValueError) 66 | assert not g.is_alive() 67 | 68 | def test_throw_goes_to_original_parent(self): 69 | main = fibers.current() 70 | 71 | def f1(): 72 | try: 73 | main.switch("f1 ready to catch") 74 | except IndexError: 75 | return "caught" 76 | else: 77 | return "normal exit" 78 | 79 | def f2(): 80 | main.switch("from f2") 81 | 82 | g1 = Fiber(f1) 83 | g2 = Fiber(target=f2, parent=g1) 84 | with pytest.raises(IndexError): 85 | g2.throw(IndexError) 86 | assert not g2.is_alive() 87 | assert g1.is_alive() # g1 is skipped because it was not started 88 | 89 | def test_throw_goes_to_original_parent2(self): 90 | main = fibers.current() 91 | 92 | def f1(): 93 | try: 94 | main.switch("f1 ready to catch") 95 | except IndexError: 96 | return "caught" 97 | else: 98 | return "normal exit" 99 | 100 | def f2(): 101 | main.switch("from f2") 102 | 103 | g1 = Fiber(f1) 104 | g2 = Fiber(target=f2, parent=g1) 105 | res = g1.switch() 106 | assert res == "f1 ready to catch" 107 | res = g2.throw(IndexError) 108 | assert res == "caught" 109 | assert not g2.is_alive() 110 | assert not g1.is_alive() 111 | 112 | def test_throw_goes_to_original_parent3(self): 113 | main = fibers.current() 114 | 115 | def f1(): 116 | try: 117 | main.switch("f1 ready to catch") 118 | except IndexError: 119 | return "caught" 120 | else: 121 | return "normal exit" 122 | 123 | def f2(): 124 | main.switch("from f2") 125 | 126 | g1 = Fiber(f1) 127 | g2 = Fiber(target=f2, parent=g1) 128 | res = g1.switch() 129 | assert res == "f1 ready to catch" 130 | res = g2.switch() 131 | assert res == "from f2" 132 | res = g2.throw(IndexError) 133 | assert res == "caught" 134 | assert not g2.is_alive() 135 | assert not g1.is_alive() 136 | 137 | 138 | if __name__ == '__main__': 139 | unittest.main(verbosity=2) 140 | 141 | -------------------------------------------------------------------------------- /tests/test_weakref.py: -------------------------------------------------------------------------------- 1 | 2 | import gc 3 | import weakref 4 | import unittest 5 | 6 | import os 7 | import sys 8 | 9 | import fibers 10 | from fibers import Fiber 11 | 12 | 13 | class WeakRefTests(unittest.TestCase): 14 | def test_dead_weakref(self): 15 | def _dead_fiber(): 16 | g = Fiber(lambda: None) 17 | g.switch() 18 | return g 19 | o = weakref.ref(_dead_fiber()) 20 | gc.collect() 21 | assert o() == None 22 | 23 | def test_inactive_weakref(self): 24 | o = weakref.ref(Fiber()) 25 | gc.collect() 26 | assert o() == None 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main(verbosity=2) 31 | 32 | -------------------------------------------------------------------------------- /tests/verify-version.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import fibers 4 | 5 | expected_version = sys.argv[1].strip() 6 | assert(expected_version[0:7] == 'fibers-') 7 | expected_version = expected_version[7:] 8 | 9 | print("assert '%s' == '%s'" % (fibers.__version__, expected_version,)) 10 | assert fibers.__version__ == expected_version 11 | 12 | --------------------------------------------------------------------------------