Skip to content

Add codecs support to c-api#8267

Open
bschoenmaeckers wants to merge 2 commits into
RustPython:mainfrom
bschoenmaeckers:c-api-codecs
Open

Add codecs support to c-api#8267
bschoenmaeckers wants to merge 2 commits into
RustPython:mainfrom
bschoenmaeckers:c-api-codecs

Conversation

@bschoenmaeckers

@bschoenmaeckers bschoenmaeckers commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Expanded the C API with codec registration, discovery, encoding/decoding, and incremental/stream processing.
    • Added support for codec error handlers (register/lookup) and built-in modes such as strict/ignore/replace and related escaping strategies.
  • Bug Fixes
    • Improved handling of C string inputs (including null pointers and invalid UTF-8) across common API entry points, resulting in more consistent error behavior.
  • Chores
    • Minor formatting/comment cleanup.

@bschoenmaeckers bschoenmaeckers changed the title Add codecs support to c-apo Add codecs support to c-api Jul 13, 2026
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: f7f94de3-6e6c-4046-b9cc-1149b6a5215f

📥 Commits

Reviewing files that changed from the base of the PR and between 26a957c and ecb2046.

📒 Files selected for processing (14)
  • crates/capi/src/abstract_.rs
  • crates/capi/src/abstract_/mapping.rs
  • crates/capi/src/codecs.rs
  • crates/capi/src/descrobject.rs
  • crates/capi/src/dictobject.rs
  • crates/capi/src/import.rs
  • crates/capi/src/lib.rs
  • crates/capi/src/methodobject.rs
  • crates/capi/src/object.rs
  • crates/capi/src/pycapsule.rs
  • crates/capi/src/pyerrors.rs
  • crates/capi/src/util.rs
  • crates/capi/src/warnings.rs
  • crates/vm/src/sequence.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/capi/src/lib.rs
  • crates/capi/src/codecs.rs

📝 Walkthrough

Walkthrough

Adds a public codecs C-API module with codec registry operations and centralizes VM-aware C-string UTF-8 conversion across existing C-API entry points.

Changes

C API codec bridge and string conversion

Layer / File(s) Summary
Codec registry bridge
crates/capi/src/codecs.rs, crates/capi/src/lib.rs
Exports codec registration, lookup, encode/decode, incremental and stream construction, and error-handler functions backed by codec_registry.
Shared C-string conversion contract
crates/capi/src/util.rs
Adds CStrExt::try_as_str for nullable C-string handling and VM-aware UTF-8 validation.
Object, mapping, and dictionary string APIs
crates/capi/src/abstract_.rs, crates/capi/src/abstract_/mapping.rs, crates/capi/src/dictobject.rs, crates/capi/src/object.rs
Updates string-key and attribute APIs to use the shared conversion helper.
Metadata, import, error, and warning conversion
crates/capi/src/descrobject.rs, crates/capi/src/methodobject.rs, crates/capi/src/import.rs, crates/capi/src/pycapsule.rs, crates/capi/src/pyerrors.rs, crates/capi/src/warnings.rs
Replaces manual C-string decoding in descriptor, import, capsule, error, warning, and method-definition paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: youknowone

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding codec support to the C API.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@youknowone youknowone left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you also check if other boilerplates can be also refactored?

Comment thread crates/capi/src/codecs.rs Outdated
} else {
unsafe { CStr::from_ptr(encoding) }
.to_str()
.map_err(|_| vm.new_system_error("encoding must be valid UTF-8"))?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a helper for this commonly used error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a helper. What do you think?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
crates/capi/src/util.rs (1)

232-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix typographical error in the exception message.

The message "argument of must be valid UTF-8" appears to be missing a word or incorrectly includes "of". Consider changing it to "argument must be valid UTF-8".

♻️ Proposed fix
 impl<'a> CStrExt<'a> for *mut c_char {
     fn try_as_str(self, vm: &VirtualMachine) -> PyResult<Option<&'a str>> {
         NonNull::new(self)
             .map(|ptr| unsafe { CStr::from_ptr(ptr.as_ptr()) }.to_str())
             .transpose()
-            .map_err(|_| vm.new_system_error("argument of must be valid UTF-8"))
+            .map_err(|_| vm.new_system_error("argument must be valid UTF-8"))
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/util.rs` around lines 232 - 239, Update the error message in
CStrExt::try_as_str to remove the extraneous “of”, so invalid UTF-8 reports that
the argument must be valid UTF-8.
crates/capi/src/import.rs (1)

52-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify null pointer check.

Since try_as_str(vm)? naturally handles null pointers by returning Option::None, you can simplify this block using if let instead of explicitly checking for .is_null() and calling .unwrap().

♻️ Proposed fix
-        if !pathname.is_null() {
-            let pathname = pathname.try_as_str(vm)?.unwrap();
+        if let Some(pathname) = pathname.try_as_str(vm)? {
             module.set_attr("__file__", vm.ctx.new_str(pathname), vm)?;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/import.rs` around lines 52 - 53, Update the pathname handling
block to use `if let Some(pathname) = pathname.try_as_str(vm)?` directly,
removing the explicit `is_null()` check and `unwrap()` while preserving the
existing processing for valid pathnames.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/capi/src/descrobject.rs`:
- Around line 229-234: Update PyDescr_NewMember to return the result of
member.build directly from the with_vm closure, removing the wrapping Ok so the
existing with_vm conversion path receives the expected result type.

In `@crates/capi/src/pyerrors.rs`:
- Around line 211-216: Update the exception-name parsing in the with_vm closure
to propagate the PyResult from try_as_str(vm) with ?, removing the expect that
panics on invalid UTF-8; preserve the existing unwrap and rsplit_once validation
behavior.

---

Nitpick comments:
In `@crates/capi/src/import.rs`:
- Around line 52-53: Update the pathname handling block to use `if let
Some(pathname) = pathname.try_as_str(vm)?` directly, removing the explicit
`is_null()` check and `unwrap()` while preserving the existing processing for
valid pathnames.

In `@crates/capi/src/util.rs`:
- Around line 232-239: Update the error message in CStrExt::try_as_str to remove
the extraneous “of”, so invalid UTF-8 reports that the argument must be valid
UTF-8.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: f7f94de3-6e6c-4046-b9cc-1149b6a5215f

📥 Commits

Reviewing files that changed from the base of the PR and between 26a957c and ecb2046.

📒 Files selected for processing (14)
  • crates/capi/src/abstract_.rs
  • crates/capi/src/abstract_/mapping.rs
  • crates/capi/src/codecs.rs
  • crates/capi/src/descrobject.rs
  • crates/capi/src/dictobject.rs
  • crates/capi/src/import.rs
  • crates/capi/src/lib.rs
  • crates/capi/src/methodobject.rs
  • crates/capi/src/object.rs
  • crates/capi/src/pycapsule.rs
  • crates/capi/src/pyerrors.rs
  • crates/capi/src/util.rs
  • crates/capi/src/warnings.rs
  • crates/vm/src/sequence.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/capi/src/lib.rs
  • crates/capi/src/codecs.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🧹 Nitpick comments (2)
crates/capi/src/util.rs (1)

232-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix typographical error in the exception message.

The message "argument of must be valid UTF-8" appears to be missing a word or incorrectly includes "of". Consider changing it to "argument must be valid UTF-8".

♻️ Proposed fix
 impl<'a> CStrExt<'a> for *mut c_char {
     fn try_as_str(self, vm: &VirtualMachine) -> PyResult<Option<&'a str>> {
         NonNull::new(self)
             .map(|ptr| unsafe { CStr::from_ptr(ptr.as_ptr()) }.to_str())
             .transpose()
-            .map_err(|_| vm.new_system_error("argument of must be valid UTF-8"))
+            .map_err(|_| vm.new_system_error("argument must be valid UTF-8"))
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/util.rs` around lines 232 - 239, Update the error message in
CStrExt::try_as_str to remove the extraneous “of”, so invalid UTF-8 reports that
the argument must be valid UTF-8.
crates/capi/src/import.rs (1)

52-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify null pointer check.

Since try_as_str(vm)? naturally handles null pointers by returning Option::None, you can simplify this block using if let instead of explicitly checking for .is_null() and calling .unwrap().

♻️ Proposed fix
-        if !pathname.is_null() {
-            let pathname = pathname.try_as_str(vm)?.unwrap();
+        if let Some(pathname) = pathname.try_as_str(vm)? {
             module.set_attr("__file__", vm.ctx.new_str(pathname), vm)?;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/import.rs` around lines 52 - 53, Update the pathname handling
block to use `if let Some(pathname) = pathname.try_as_str(vm)?` directly,
removing the explicit `is_null()` check and `unwrap()` while preserving the
existing processing for valid pathnames.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/capi/src/descrobject.rs`:
- Around line 229-234: Update PyDescr_NewMember to return the result of
member.build directly from the with_vm closure, removing the wrapping Ok so the
existing with_vm conversion path receives the expected result type.

In `@crates/capi/src/pyerrors.rs`:
- Around line 211-216: Update the exception-name parsing in the with_vm closure
to propagate the PyResult from try_as_str(vm) with ?, removing the expect that
panics on invalid UTF-8; preserve the existing unwrap and rsplit_once validation
behavior.

---

Nitpick comments:
In `@crates/capi/src/import.rs`:
- Around line 52-53: Update the pathname handling block to use `if let
Some(pathname) = pathname.try_as_str(vm)?` directly, removing the explicit
`is_null()` check and `unwrap()` while preserving the existing processing for
valid pathnames.

In `@crates/capi/src/util.rs`:
- Around line 232-239: Update the error message in CStrExt::try_as_str to remove
the extraneous “of”, so invalid UTF-8 reports that the argument must be valid
UTF-8.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: f7f94de3-6e6c-4046-b9cc-1149b6a5215f

📥 Commits

Reviewing files that changed from the base of the PR and between 26a957c and ecb2046.

📒 Files selected for processing (14)
  • crates/capi/src/abstract_.rs
  • crates/capi/src/abstract_/mapping.rs
  • crates/capi/src/codecs.rs
  • crates/capi/src/descrobject.rs
  • crates/capi/src/dictobject.rs
  • crates/capi/src/import.rs
  • crates/capi/src/lib.rs
  • crates/capi/src/methodobject.rs
  • crates/capi/src/object.rs
  • crates/capi/src/pycapsule.rs
  • crates/capi/src/pyerrors.rs
  • crates/capi/src/util.rs
  • crates/capi/src/warnings.rs
  • crates/vm/src/sequence.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/capi/src/lib.rs
  • crates/capi/src/codecs.rs
🛑 Comments failed to post (2)
crates/capi/src/descrobject.rs (1)

229-234: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
printf '\n== descrobject.rs excerpt ==\n'
sed -n '200,260p' crates/capi/src/descrobject.rs

printf '\n== with_vm definition/usages ==\n'
rg -n "fn with_vm|with_vm\(" -S crates | sed -n '1,120p'

printf '\n== build() signatures near PyMemberDescriptor ==\n'
rg -n "build\(" crates/capi/src/descrobject.rs crates -S | sed -n '1,120p'

Repository: RustPython/RustPython

Length of output: 12962


🏁 Script executed:

set -euo pipefail
echo '=== descrobject.rs (220-240) ==='
sed -n '220,240p' crates/capi/src/descrobject.rs

echo
echo '=== search with_vm definition ==='
rg -n "pub .*with_vm|fn with_vm|with_vm\s*\(" crates -S | sed -n '1,120p'

echo
echo '=== search build signature for PyMemberDef ==='
rg -n "struct PyMemberDef|impl .*PyMemberDef|build\(.*PyTypeObject|PyResult<PyRef<PyMemberDescriptor>>" crates -S | sed -n '1,160p'

Repository: RustPython/RustPython

Length of output: 9132


🏁 Script executed:

set -euo pipefail

echo '=== crates/vm/src/vm/thread.rs (with_vm) ==='
sed -n '740,810p' crates/vm/src/vm/thread.rs

echo
echo '=== crates/capi/src/descrobject.rs (PyMemberDef build) ==='
sed -n '120,155p' crates/capi/src/descrobject.rs

echo
echo '=== nearby patterns in crates/capi/src ==='
rg -n "with_vm\\(\\|.*Ok\\(.*build\\(" crates/capi/src -S | sed -n '1,120p'

Repository: RustPython/RustPython

Length of output: 4154


🏁 Script executed:

set -euo pipefail

echo '=== crates/capi/src/descrobject.rs (PyGetSetDef build) ==='
sed -n '1,80p' crates/capi/src/descrobject.rs

echo
echo '=== crates/capi/src/descrobject.rs (PyMemberDef build continued) ==='
sed -n '80,180p' crates/capi/src/descrobject.rs

echo
echo '=== functions in descrobject.rs using with_vm + Ok(...) ==='
rg -n "with_vm\\(\\|\\s*Ok\\(" crates/capi/src/descrobject.rs -n -S

Repository: RustPython/RustPython

Length of output: 7393


🏁 Script executed:

set -euo pipefail

echo '=== crates/capi/src/descrobject.rs (1-180) ==='
sed -n '1,180p' crates/capi/src/descrobject.rs

echo
echo '=== other with_vm + Ok(...) patterns in capi ==='
rg -n "with_vm\\(\\|\\s*Ok\\(" crates/capi/src -S | sed -n '1,200p'

Repository: RustPython/RustPython

Length of output: 7223


🏁 Script executed:

set -euo pipefail

echo '=== crates/capi/src/pystate.rs ==='
sed -n '1,220p' crates/capi/src/pystate.rs

echo
echo '=== direct uses of crate::pystate::with_vm in capi ==='
rg -n "crate::pystate::with_vm|with_vm\\(" crates/capi/src -S | sed -n '1,220p'

Repository: RustPython/RustPython

Length of output: 18885


🏁 Script executed:

set -euo pipefail

echo '=== crates/capi/src/util.rs ==='
sed -n '1,220p' crates/capi/src/util.rs

echo
echo '=== FfiResult implementations ==='
rg -n "trait FfiResult|impl .*FfiResult" crates/capi/src crates -S | sed -n '1,220p'

Repository: RustPython/RustPython

Length of output: 7911


Return build() directly in PyDescr_NewMember. The extra Ok(...) adds a nested Result and breaks the with_vm conversion path here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/descrobject.rs` around lines 229 - 234, Update
PyDescr_NewMember to return the result of member.build directly from the with_vm
closure, removing the wrapping Ok so the existing with_vm conversion path
receives the expected result type.
crates/capi/src/pyerrors.rs (1)

211-216: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate the PyResult rather than panicking.

Calling .expect("...") on try_as_str(vm) triggers a Rust panic if the input is not valid UTF-8. Since we are inside a with_vm closure, propagating the result via ? will correctly translate the failure into a Python exception state.

♻️ Proposed fix
         let (module, name) = name
-            .try_as_str(vm)
-            .expect("Exception name is not valid UTF-8")
-            .unwrap()
+            .try_as_str(vm)?
+            .unwrap()
             .rsplit_once('.')
             .expect("Exception name must be of the form 'module.ExceptionName'");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let (module, name) = name
            .try_as_str(vm)?
            .unwrap()
            .rsplit_once('.')
            .expect("Exception name must be of the form 'module.ExceptionName'");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/pyerrors.rs` around lines 211 - 216, Update the
exception-name parsing in the with_vm closure to propagate the PyResult from
try_as_str(vm) with ?, removing the expect that panics on invalid UTF-8;
preserve the existing unwrap and rsplit_once validation behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants