如何在Squeak FFI中处理多级指针(如char**)及结构体字段
Great question! Squeak's FFI can be tricky when dealing with multi-level pointers because the default field definitions don't automatically distinguish between char* and char**. Let's walk through how to fix this and properly map your C struct to a Squeak ExternalStructure.
The Problem Context
First, let's recap your setup for clarity:
Your C struct:
struct foo { char *name; char **fields; size_t nfields; };
Your initial Squeak ExternalStructure subclass:
ExternalStructure subclass: #Foo instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'FFI-Tests'.
Field definition:
Foo class>>fields ^#( (name 'char*') (fields 'char**') (nfields 'unsigned long') )
The issue here is that Squeak's default field parser treats both char* and char** as generic pointers (mapped to ExternalAddress), but doesn't handle the extra level of indirection for char**—so you can't directly get an array of strings from it without custom code.
Solution Steps
1. Adjust Field Definitions for Explicit Pointer Handling
Instead of relying on the 'char*'/'char**' type strings, use 'void*' for both pointer fields. This maps directly to ExternalAddress, which gives you low-level access to manipulate the pointer values, and lets you build custom conversion logic on top:
Foo class>>fields ^#( (name 'void*') (fields 'void*') (nfields 'unsigned long') )
Run Foo defineFields again to generate base accessors, then we'll override them to add the pointer-level logic.
2. Customize Accessors for name (char* ↔ String)
For the single char* pointer, we want to convert between the external address and a Squeak string. Override the auto-generated accessors:
Foo>>name | addr | addr := self getHandle pointerAt: 1. "Get the external address of the name string" addr isNull ifTrue: [^'']. ^addr readStringUTF8 "Convert null-terminated C string to Squeak string"
Foo>>name: aString | addr | addr := aString ifEmpty: [ExternalAddress null] ifNotEmpty: [ExternalAddress allocateStringUTF8: aString]. self getHandle pointerAt: 1 put: addr. "Note: If this struct is passed to C code that owns the memory, you might need to avoid allocating here—adjust based on your memory ownership rules"
3. Customize Accessors for fields (char** ↔ Array of Strings)
For the double pointer char**, we need to treat it as an array of char* pointers, each pointing to a string. Here's how to implement the accessors:
Foo>>fields | fieldsAddr resultArray | fieldsAddr := self getHandle pointerAt: (Foo fieldOffsets at: #fields). "Use field offset to avoid hardcoding" fieldsAddr isNull ifTrue: [^#()]. resultArray := Array new: self nfields. 1 to: self nfields do: [:index | | strAddr | "Calculate offset for the nth pointer in the array (each pointer is wordSize bytes)" strAddr := fieldsAddr pointerAt: ((index - 1) * ExternalAddress wordSize) + 1. resultArray at: index put: (strAddr isNull ifTrue: [''] ifFalse: [strAddr readStringUTF8]) ]. ^resultArray
Foo>>fields: aStringArray | fieldsAddr strAddresses | "Allocate addresses for each string" strAddresses := aStringArray collect: [:str | str ifEmpty: [ExternalAddress null] ifNotEmpty: [ExternalAddress allocateStringUTF8: str] ]. "Allocate memory for the array of pointers" fieldsAddr := ExternalAddress allocate: (aStringArray size * ExternalAddress wordSize). "Write each string's address into the pointer array" 1 to: aStringArray size do: [:index | fieldsAddr pointerAt: ((index - 1) * ExternalAddress wordSize) + 1 put: (strAddresses at: index) ]. "Update the struct's fields pointer and nfields count" self getHandle pointerAt: (Foo fieldOffsets at: #fields) put: fieldsAddr. self nfields: aStringArray size.
4. Add Memory Management (Critical!)
Squeak doesn't automatically free external memory you allocate, so you should add a method to clean up the struct's resources when you're done with it:
Foo>>free | fieldsAddr | "Free the name string" (self getHandle pointerAt: (Foo fieldOffsets at: #name)) isNull ifFalse: [:addr | addr free]. "Free each string in fields, then the pointer array" fieldsAddr := self getHandle pointerAt: (Foo fieldOffsets at: #fields). fieldsAddr isNull ifFalse: [ 1 to: self nfields do: [:index | | strAddr | strAddr := fieldsAddr pointerAt: ((index - 1) * ExternalAddress wordSize) + 1. strAddr isNull ifFalse: [strAddr free] ]. fieldsAddr free ]. "Free the struct's own memory if it was allocated externally" self getHandle free
5. Verify Memory Layout
To make sure you're using the correct offsets for each field, run:
Foo fieldOffsets
This returns a dictionary like {#name->0, #fields->8, #nfields->16} (for 64-bit Squeak), which confirms where each field lives in the struct's memory.
Key Takeaway
The default ExternalStructure field definitions work for simple types, but multi-level pointers need custom accessor logic to bridge the gap between external C memory and Squeak objects. By using ExternalAddress directly and building conversion methods, you can properly distinguish between single and double pointers, and safely manage the data and memory associated with your struct.
内容的提问来源于stack exchange,提问作者aka.nice

