Reputation: 300
I am writing a C DLL that creates a new command:
Tcl_CreateObjCommand( interp, "work_on_dict", (Tcl_ObjCmdProc *)work_on_dict_cmd, (ClientData)NULL, (Tcl_CmdDeleteProc *)NULL );
The implementation of the command is:
int work_on_dict_cmd( ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[] )
{
Tcl_Obj *dReturn = Tcl_DuplicateObj( objv[1] ); <------------------- duplicate
Tcl_DictObjPut( interp, dReturn, Tcl_NewStringObj("key2", -1), Tcl_NewIntObj(2) );
Tcl_SetObjResult( interp, dReturn );
return TCL_OK;
}
The Tcl code calling it looks like this:
set dDict [dict create]
dict set dDict "key1" 1
set dDict [work_on_dict $dDict] <------------------- assign back
puts "[dict get $dDict "key1"] [dict get $dDict "key2"]"
Before altering the dictionary in the C code, I have to duplicate it. Otherwise I get a "called with shared object" error. So I have to assign the returned dictionary back to the original one in the Tcl code.
I wonder if there is'nt a smarter way, allowing the C code to work directly on the original dictionary. Something like "work_on_dict dDict" in Tcl and dereferencing the parameter in the C code.
I tried a lot of things but did'nt come to a conclusion if this is after all possible.
I would be glad if someone could give me a hint, thanks.
Here is the solution - Derived from Donal's answer:
C code:
Note that you should add error checking, probably limit the search range to TCL_NAMESPACE_ONLY and call Tcl_ObjSetVar2 for correct traces as Donal states.
int work_on_dict_cmd( ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[] )
{
Tcl_Obj *dReturn = Tcl_ObjGetVar2( interp, objv[1], NULL, 0 );
Tcl_DictObjPut( interp, dReturn, Tcl_NewStringObj("key2", -1), Tcl_NewIntObj(2) );
Tcl_SetObjResult( interp, dReturn );
return TCL_OK;
}
Tcl:
set dDict [dict create]
dict set dDict "key1" 1
work_on_dict dDict
puts "[dict get $dDict "key1"] [dict get $dDict "key2"]"
Upvotes: 2
Views: 167
Reputation: 137587
In the way you're calling your command, you've got a minimum of two references to the dictionary, one from the variable holding it, and another from the argument stack for the duration of the call. There may be others too, of course.
The standard way of fixing things would be for you to make the Tcl_DuplicateObj(dictPtr)
call conditional on whether Tcl_IsShared(dictPtr)
returns true. That would then let you call your code like this for efficient working:
set dDict [work_on_dict $dDict[set dDict {}]]
Yes, that's ugly. It's easier to understand if we use this older version which uses a “well-known” combinator: K.
proc K {x y} {return $x}
set dDict [work_on_dict [K $dDict [set dDict anyOldConstantValue]]]
The contortion with K
etc ends up transferring the reference to the argument stack only, removing the one held in the variable, so that then your command is called efficiently. The amazing/horrifying version which looks crazy actually does the same, but purely in bytecode:
% tcl::unsupported::disassemble script {set dDict [work_on_dict $dDict[set dDict {}]]}
ByteCode 0x0x1008aee10, refCt 1, epoch 15, interp 0x0x100829a10 (epoch 15)
Source "set dDict [work_on_dict $dDict[set dDict {}]]"
Cmds 3, src 45, inst 27, litObjs 3, aux 0, stkDepth 5, code/src 0.00
Commands 3:
1: pc 0-25, src 0-44 2: pc 2-24, src 11-43
3: pc 7-20, src 31-42
Command 1: "set dDict [work_on_dict $dDict[set dDict {}]]"
(0) push1 0 # "dDict"
Command 2: "work_on_dict $dDict[set dDict {}]"
(2) push1 1 # "work_on_dict"
(4) push1 0 # "dDict"
(6) loadStk
Command 3: "set dDict {}"
(7) startCommand +14 1 # next cmd at pc 21
(16) push1 0 # "dDict"
(18) push1 2 # ""
(20) storeStk
(21) concat1 2
(23) invokeStk1 2
(25) storeStk
(26) done
The concat1
opcode has an optimization that means that if the second string is an empty string literal, it does nothing.
Commands that are commonly doing dictionary (or list) modification usually take the name of a variable containing the dict. (For example, that's what dict set
and dict unset
do.) Because reading from a variable on the C side (probably using Tcl_ObjGetVar2
) doesn't change the reference count, if the value is only held in the variable, you will be able to update it directly. You should also call Tcl_ObjSetVar2
after doing the write so that any traces on the variable fire.
With that, you'd change your command to be called like this:
work_on_dict dDict
Note that there's no $
there; you're passing in the variable's name and not it's contents.
Upvotes: 1