新闻详情

新闻详情

首页 / 资讯中心 / 详情

SystemVerilog深拷贝与clone函数详解:避开句柄复制的坑

发布时间:2026/10/1 13:58:05来源:尧图网络
SystemVerilog深拷贝与clone函数详解:避开句柄复制的坑
我们部门做验证平台维护的时候遇到过这样一个case一组差分类的回归天天过某天把所有seed跑了一遍忽然冒出一堆报错定位到reference model发现某个transactor里用“”把一个transaction句柄直接赋给另一个对象成员后续在scoreboard里改了这个句柄的数据结果原先的transaction也被改掉了。这种问题在SystemVerilog下面直接叫SV里可以说是最常见的OOP坑之一——句柄复制、浅拷贝、深拷贝这几个概念没吃透写出来的验证环境越大越容易翻车。这篇文章我就把这几个概念掰开讲清楚重点放在SV里怎么正确实现深拷贝以及clone函数的标准写法。适合刚接触SV OOP的验证工程师也适合被UVM的clone()坑过、想彻底搞明白“到底复制了多少数据”的同学。1. 句柄复制先弄清楚“”到底复制了什么1.1 对象句柄与对象实例的关系在SV里定义一个类很简单class transaction; rand bit [7:0] addr; rand bit [31:0] data; string name; endclass接着实例化transaction t1; t1 new();很多人会把t1 new()理解成“创建了一个transaction对象”严格说不对。new()会在仿真器的堆区分配一块内存然后把这块内存的地址返回给t1。t1本身只是一个“句柄”你可以把它理解成C语言的指针或者Java里的引用变量。真正装数据的对象实体是另一块独立存在的东西。所以当我们再写transaction t2; t2 t1;这儿发生了什么右边的t1取出它保存的地址把这个地址复制给t2。复制完成后t1和t2指向同一个堆对象。这块对象实例没被复制成两份复制出来的只是一个“指向同一块数据”的句柄。反过来看C语言里面结构体赋值struct transaction t2 t1;C结构体这样写会把整个内存内容逐字节复制得到两份独立的数据。但在SV里class的赋值语义是“复制句柄”不是“复制整个对象”。SV语法上根本不允许你用直接把一个class对象整体复制给另一个class变量——除非你去专门实现复制函数。这个差异是后面一大堆容易绕晕的问题的根源。1.2 一个句柄被改了另一个也变了写个最简单的例子module tb; transaction t1; transaction t2; initial begin t1 new(); t1.addr 8h10; t2 t1; t2.addr 8h20; $display(t1.addr %0h, t1.addr); $display(t2.addr %0h, t2.addr); end endmodule结果两个句柄打印出来都是20。原因不用多说t1和t2都是同一块对象实体的“门牌号”从任何一个门进去改东西另一边的门看到的自然也是改动后的状态。这很像两个人共同拥有一张银行卡任何一方刷卡消费余额对两个人来说都是同步变化的。大多数验证环境的bug都是因为“我以为我复制了一份独立的数据实际上只是给同一个对象多加了一个句柄”。还有更隐蔽的情况同一个对象被放进多个队列里。transaction mem[$]; transaction cfg; cfg new(); cfg.addr 8h1; mem.push_back(cfg); cfg.addr 8h2; mem.push_back(cfg);看起来好像往队列里放了两份不同配置实际上队列里两个元素指向同一个对象两次push_back之后队列里全是addr8h2的那个配置。这种问题在sequence、config_db配置类和scoreboard存储里都很容易遇到。1.3 句柄复制、浅拷贝、深拷贝的区别“句柄复制”和“浅拷贝”经常被混着说但从工程角度还是得分清楚。t2 t1;只是复制了地址这叫句柄复制。至少在当前语境下它没有创建新对象。严格来说它甚至不配叫“拷贝”它只是拷贝了一个引用。真正的浅拷贝是创建出一个新的对象实例把源对象里的每个成员变量逐个复制到新对象中。但这个“逐个复制”遇到class类型的成员时因为SV赋值语义的限制实际上只复制了内部成员对象的句柄。也就是说外层对象是新的内层嵌套对象还是共享同一份。深拷贝则要求从外层到内层全部复制一遍。遇到嵌套的class成员要递归地new一个新对象把嵌套对象内部的成员也全部复制遇到动态数组、队列、关联数组同样需要逐个复制元素遇到元素本身是class类型的数组还得继续递归。用一个生活化的比喻浅拷贝相当于我买了一本书然后用复印机把封面复印了一份贴在我自己的本子上里面内容还是从你那本书里撕过来的改里面的文字咱们俩都受影响。深拷贝则是从头到尾重新排版印制了一本后续怎么改都不影响原版。表格总结一下操作方式是否创建新对象外层成员是否独立嵌套class成员是否独立典型写法句柄复制否共享完全共享t2 t1;浅拷贝是独立共享手写shallow copy函数深拷贝是独立各自独立递归实现deep copy或clone明白了这个区别就能理解为什么验证环境里“想复制一份transaction”不能靠简单的或者随便写个copy函数草草了事。2. 浅拷贝与深拷贝新杯子装旧酒还是连酒一起换2.1 为什么先有浅拷贝手动逐字段复制手写浅拷贝的逻辑不难理解new一个新对象然后把源对象的每个成员赋值到新对象上。难点在于怎么处理不同类型成员。SV的成员类型五花八门整型、bit类型、枚举、字符串、动态数组、队列、关联数组还有嵌套class句柄。做浅拷贝时最直接的做法是“见啥复制啥”class payload; rand int len; rand byte data[]; endclass class packet; rand bit [7:0] addr; rand bit [31:0] data; string name; payload pld; function packet shallow_copy(); packet c new(); c.addr this.addr; c.data this.data; c.name this.name; c.pld this.pld; // 只复制句柄pld内部还是同一个对象 return c; endfunction endclass这样写完之后c.pld和this.pld指向同一个payload对象。如果只是把packet当广播消息传来传去不修改pld内部字段可能看不出问题。但一旦有人往c.pld.data[0]里写了个新值原始packet的payload也会跟着变这就是典型的浅拷贝副作用。String在实际使用中经常被当成普通成员处理这点倒是问题不大。SV里的string类型是可变长、动态分配的但它的赋值语义比较接近值语义——把一个字符串赋给另一个string变量后两个变量互不干扰。这一点和某些语言里的对象引用不太一样所以浅拷贝和深拷贝对string的处理通常没有区别不用为string专门做“逐字符复制”。2.2 深拷贝给每个层级都做一次“新建再复制”深拷贝必须处理嵌套结构。对于上面的类手动深拷贝可以写成function packet deep_copy(); packet c new(); c.addr this.addr; c.data this.data; c.name this.name; if (this.pld null) begin c.pld null; end else begin c.pld new(); c.pld.len this.pld.len; if (this.pld.data ! null) begin c.pld.data new[this.pld.data.size()]; foreach (this.pld.data[i]) begin c.pld.data[i] this.pld.data[i]; end end end return c; endfunction注意几个细节第一c.pld new()之前要先判断this.pld是否为空否则源对象里是null你到目标对象里新建一个pld结果把“空指针”错误地变成了“有对象”的状态。第二动态数组data必须要用new[this.pld.data.size()]重新分配空间再一个个元素复制。直接用赋值对动态数组来说也只是复制句柄或者说复制数组描述符两个数组还是指向同一块存储区。第三如果数组元素本身又是class对象比如payload payloads[];那foreach里还得递归new一个子对象再做复制。这就说明深拷贝不是写完就算得严格审查自己类里哪一层还有class句柄、哪一层还有数组。2.3 不同成员类型在深拷贝里怎么处理我把常见成员类型和深拷贝建议整理成一个速查表成员类型复制方式典型注意事项bit/logic/int/enum直接无string直接SV的string赋值是值语义不影响动态数组先new[size]再逐元素赋值不要直接直接只是复制数组句柄队列用delete()清空目标队列再push_back或for循环逐个复制队列的赋值在SV里也是浅拷贝至少对队列元素来说是共享的关联数组foreach遍历源数组逐项插入目标数组需要先用.delete()清空目标数组class句柄先判断null再递归复制一个子对象最容易被忽略也是浅拷贝的经典场景数组里的class句柄需要循环内再递归new每层都不能少static成员通常不用复制属于类全局数据所有实例共享这里要强调一下DMA数组那类操作。验证环境里最常见的transaction成员是byte data[]也就是动态数组。如果你在copy函数里直接写c.data this.data;看起来合法实际上data这种动态数组在SV里本质上是一个“对象句柄”式的聚合类型这种写法复制的是数组描述符两个数组底层是同一块存储后续往里面写数据会互相踩。正确做法永远是先建一块独立空间再逐元素复制。对大数据包来说这个循环会有点开销但这是保证隔离性的代价。2.4 顺带说说JS/ES6里的浅拷贝与深拷贝搜索热词里很多人会同时搜“js浅拷贝 vs 深拷贝”“es6深拷贝”因为JavaScript里也有完全类似的坑Object.assign({}, obj)是浅拷贝对象内部的嵌套对象还是共享引用JSON.parse(JSON.stringify(obj))常被用来做深拷贝但有原型丢失、函数丢失等问题。在SV里没有structuredClone这种内置方法所以我们只能老老实实写递归复制。这种跨语言的类比其实很有用。理解了SV里的句柄复制等于理解了很多编程语言中“引用赋值”的统一模型。区别只是在SV的UVM验证环境中复制逻辑往往还要配套工厂、类型转换和多态复杂度比前端项目更深一层。3. clone函数把复制行为封装成对象自己的职责3.1 为什么需要clone函数在验证环境里transaction类越来越多成员越来越复杂。要是每次想复制一个对象就在外部写一堆逐字段赋值的代码代码很快就臭了。更合理的做法是给每个类提供一个clone()方法让对象自己知道怎么复制自己。clone()有两个用途需要一个功能和原对象相同但内存独立的新对象。提供一个类级别的通用接口让上层代码不关心具体复制逻辑。在UVM里transaction通常继承自uvm_sequence_item而uvm_object本身已经定义了clone()虚方法。标准做法是结合create()和copy()两个方法实现clone()。但直接用UVM自带机制有个前提你必须正确重写do_copy()或者copy()。很多人以为继承了UVM类之后clone()就自动是深拷贝这是天大的误会。UVM的clone()只是调用create()创建一个空对象再调用copy()把成员复制过去至于你的copy()里到底复制的浅还是深完全取决于你怎么实现。3.2 手动写一个标准的clone函数假设我们不引UVM先写一个纯SV的版本。最通用的项目实践是定义两个方法function void copy(input packet src); this.addr src.addr; this.data src.data; this.name src.name; if (src.pld null) this.pld null; else begin if (this.pld null) this.pld new(); this.pld.len src.pld.len; if (this.pld.data ! null) begin this.pld.data new[src.pld.data.size()]; foreach (src.pld.data[i]) this.pld.data[i] src.pld.data[i]; end end endfunction function packet clone(); packet c new(); c.copy(this); return c; endfunctioncopy()负责把数据从源对象搬到当前对象clone()负责创建一个新的空对象调用copy()填充然后返回新对象。这样外部调用就非常简单packet pkt_orig; packet pkt_copy; pkt_orig new(); pkt_copy pkt_orig.clone();pkt_copy和pkt_orig之间再没有任何共享内部对象怎么改都不会互相影响。这里有个容易掉坑的点clone()里如果直接packet c new();那后续对子类做复制时会丢多态信息。假设有个新类class eth_packet extends packet; rand bit [47:0] mac_addr; endclass如果eth_packet没有重写clone()而你又拿一个eth_packet对象调用clone()返回的却是packet类型对象因为clone()里写死了创建的是packet。这也是为什么UVM里clone()要先调用工厂的create()结合类型注册机制来创建正确类型的对象。纯SV环境里如果要支持多态复制就得把clone()声明成virtual并在每个子类里重写一遍或者用参数化工厂处理。3.3 UVM标准套路do_copy配合create在UVM环境中正确做法是这样的class packet extends uvm_sequence_item; uvm_object_utils(packet) rand bit [7:0] addr; rand bit [31:0] data; string name; payload pld; function new(string name packet); super.new(name); endfunction virtual function void do_copy(uvm_object rhs); packet rhs_pkt; super.do_copy(rhs); if (!$cast(rhs_pkt, rhs)) begin uvm_error(COPY, 类型不匹配无法复制) return; end this.addr rhs_pkt.addr; this.data rhs_pkt.data; this.name rhs_pkt.name; if (rhs_pkt.pld null) begin this.pld null; end else begin if (this.pld null) this.pld new(); this.pld.len rhs_pkt.pld.len; this.pld.data new[rhs_pkt.pld.data.size()]; foreach (rhs_pkt.pld.data[i]) this.pld.data[i] rhs_pkt.pld.data[i]; end endfunction virtual function uvm_object clone(); packet pkt; $cast(pkt, create(packet)); pkt.copy(this); return pkt; endfunction endclass这里最关键的是do_copy里的$cast。UVM的copy()接口参数是uvm_object类型你要复制一个具体类必须先把基类句柄转型成目标类型。如果不做cast就访问rhs_pkt.addr编译直接报错如果cast失败说明传入的对象类型根本不是packet这时候必须报错返回不能硬着头皮继续。还有一点UVM的do_copy开头一定要调用super.do_copy(rhs)。有人觉得基类里没有成员不调也没关系。但只要你的类层级上还有别的父类成员比如uvm_sequence_item里的字段不调super就丢数据。而且将来你在这个类下面再派生一个新类时新类重写do_copy也会调用你的do_copy如果你没调super整个链条就断了。3.4 带随机约束的对象克隆验证环境里常见场景是先randomize出一个符合条件的transaction然后克隆几份把其中一份的某些字段修改后送入不同通道。这里要注意一个问题clone()只复制当前的成员值它不会重新执行randomize也不会帮你跑一遍约束求解。如果你希望克隆出的新对象满足新加的约束需要复制后再次调用randomize()但这个过程不能带constraint_mode(0)之类的影响。有一种常见坑是源对象已经被randomize过了你在clone出来的对象上又强行改了addr字段结果这个字段和后续某个约束冲突导致randomize失败。这时要重新梳理约束优先级或者直接新定义约束块。另一个常见需求是“浅随机深复制”。比如我只想复制一部分字段剩下的字段允许在新对象里重新随机。项目里常见做法是先深拷贝一份然后调用对象的randomize()去重新求解但只对部分字段使用rand_mode(0)关闭随机。这不算clone本身的错误只是使用时的策略取舍。3.5 数组、队列和关联数组的复制取舍对于数组成员专业验证平台的代码里经常能看到这么一层判断如果transaction成员中的数组只是描述命令序列不会被并发修改那浅拷贝数组可能能接受如果数组会被scoreboard、covergroup或reference model后来修改就必须深拷贝。我在项目里遇到过这样的事一个包体类里有一个队列成员写copy函数时用直接赋值仿真器不报任何警告但一旦队列里的元素被后续逻辑弹出并修改原对象就错了。排查问题时贴着一层层的copy()找才发现队列复制这一步就是浅的。从那儿之后我给自己定了条规矩copy函数里凡见到队列、动态数组、class句柄一律追问一句“这个成员后续会不会被写会写就必须深复制”。附上一段通用队列深拷贝模板// 队列元素是普通类型 this.cfg_q.delete(); foreach (src.cfg_q[i]) this.cfg_q.push_back(src.cfg_q[i]); // 队列元素是class句柄 this.item_q.delete(); foreach (src.item_q[i]) begin item tmp; tmp new(); tmp.copy(src.item_q[i]); // 这里也需要递归深拷贝 this.item_q.push_back(tmp); end关联数组也是同理foreach遍历源数组再往目标数组里插入独立副本。4. 真实场景问题与排查技巧实录4.1 场景一同一个句柄被放进多个sequencer队列现象sequence里做了一个配置对象然后发到了两个不同的driver第二个driver改了若干字段结果第一个driver拿到的配置也变了。原因非常清晰这个配置对象在sequence里被赋值或者push_back到队列后实际上只有一个对象实例。两个driver手里的句柄指向同一块内存。排查这种问题时我通常先在关键位置打印%p对象句柄的地址。SV里$display(%p, obj)会打印出对象内容但如果想看句柄地址可以用$sformatf(%0d, obj)之类的方法或者简单地在两个driver里都打印同一个成员内容如果两边看到的值完全相同就要怀疑对象被共享了。解决倒不复杂在往队列里push之前先调用一次clone()生成独立副本。4.2 场景二clone后忘记$cast导致类型信息丢失现象uvm_object obj; packet pkt; obj original.clone(); pkt obj;这段代码在编译期不报错但运行时报类型转换错误。UVM的clone()返回值是uvm_object基类句柄。直接赋值给packet类型的变量是不允许的必须$cast。很多新人在拿到clone()返回值时忘了做cast然后写pkt original.clone();Eda工具直接说不能把uvm_object赋给packet。正确姿势packet pkt; uvm_object obj; obj original.clone(); if (!$cast(pkt, obj)) begin uvm_fatal(CLONE, 克隆出的对象类型不是packet) end如果用工厂的create(packet)返回的依然是uvm_object类型同样需要cast。这里没有捷径UVM设计就是要求你用cast完成下行转型。4.3 场景三浅拷贝函数漏掉static成员和constraint静态成员属于类本身不属于任何对象实例。如果事务里有static变量记录当前序号浅拷贝和深拷贝都不会自动复制它因为语义上它不属于实例。约束也是类似。很多人以为clone后新对象的约束块和原始对象一样实际上一开始确实一样约束定义在类里。但如果你在原始对象上临时打开或关闭了某些约束块这些constraint_mode状态不会复制到新对象。如果验证环境想要“复制一份相同约束状态的transaction”copy函数里还得手动反映约束状态。不过UVM的do_copy在uvm_object层并没有我记忆里自动复制constraint_mode的实现这属于不可依赖的mac行为。最稳妥是明确在设计文档里写清楚clone只负责数据成员约束状态由上层代码另外配置。4.4 场景四内存泄漏与无限递归深拷贝写得不小心可能造成无限递归。典型情况是class成员里有指向父对象或兄弟对象的“back reference”句柄class node; node parent; node children[]; endclass复制这种结构时如果无脑对parent也递归深拷贝很可能复制了父对象后又递归回子对象形成死循环。实际项目中遇到这种模型我的做法是分两类成员一类是“归属型”成员需要深拷贝另一类是“引用型”成员比如反向指针只复制句柄就够了。在复制逻辑里明确标注这两类比硬写深拷贝可靠得多。内存泄漏也常见目标对象里原来有一个非空的pld句柄你调用this.pld new()重新分配旧对象指针就丢了。虽然SV有自动内存管理机制仿真器不一定立刻回收但大量transaction反复复制、丢弃会导致仿真内存暴涨。项目上有时候仿真跑几个小时内存占用超过几十个G一部分原因就是这种无谓的重复分配。建议copy函数里先判断目标对象是否已有嵌套对象能复用就复用。问题现象常见原因排查方向两个对象同时被修改句柄复制导致共享查赋值、push_back、浅拷贝clone后类型不对忘记$cast检查返回值转型数组成员互相干扰动态数组直接赋值copy函数里改成逐元素复制复制后随机化失败约束状态没复制重新配置constraint_mode仿真内存暴涨深拷贝递归分配频繁检查嵌套对象是否可复用4.5 场景五timeslot里的句柄变化搜索热词里还有一个“sv timeslot”虽然和复制没有直接关系但在事件调度上有个连带的坑如果在fork...join_none或时钟边缘处理里复制了一个句柄然后在同一个time slot的另一个process里去修改新对象因为两个process看到的是同一个对象同样会出现跨进程干扰。换句话说时序调度会放大句柄复制的风险。所以我的经验是凡是会被多个process并发访问的transaction进队或者发送之前先做一次深拷贝让每个process持有自己的对象仿真调度就不会因为共享句柄而出现莫名其妙的竞态。5. 关于clone和对象复制的几个扩展建议5.1 copy和clone最好成对出现在自定义类的时候我建议把copy()和clone()当成一对标配方法跟new()一样属于类的基本能力。copy()负责把数据从一个已有对象复制到另一个已有对象clone()负责先new再copy。这样上层代码无论想原地覆盖还是新建副本都有明确入口。如果类层级比较复杂可以在基类里把copy声明成virtual function在子类里重写时先调用父类copy再复制子类新增成员。这个模式跟UV M的do_copy思路完全一致UPF验证环境不引UVM也可以套用。5.2 工厂优先于直接new用clone()时尽量别在函数体内直接new()具体类最好走工厂。纯SV里可以自己写一个简单的工厂class packet_factory; static function packet create_packet(); packet pkt new(); return pkt; endfunction endclass更复杂的项目可以直接用UVM工厂好处是类型注册后testbench里可以通过字符串或类型覆盖来替换实现类克隆出的对象自动就是替换后的子类。比如参考模型里要注入错误注入对象工厂根据配置生成的是bad_packet子类clone出来的依然是bad_packet不会退回基类。我实际用过之后觉得对于transaction、sequence_item这类经常被clone的类型一步到位用工厂最省心。5.3 在scoreboard和coverage模型里隔离数据scoreboard是句柄共享问题的高发区。比较逻辑刚把expected transaction存到队列里下一次循环时待测对象又被后续激励改了队列里的数据跟着变导致永远比不出来。解决办法就是要确保从监测接口拿到的每个item都是“独立副本”再用副本去排队比较。我在自己的验证环境中会在collector那个环节统一做一次clone()让后面所有模块都持有独立对象。这样一个模块改对象内容时其他模块不会感知。代价是多花一点仿真内存但换来的是逻辑隔离调试成本大大降低。5.4 两个实用原则送给大家第一个原则永远不要在返回对象句柄之后还期望调用者不会修改它。如果你要保护内部数据就返回深拷贝。第二个原则能用值类型传递的字段不要用class包一层减少嵌套引用本身就少了很多浅拷贝风险。这些原则听着朴素实际维护起来却最管用。我有一次排查一个问题查了差不多半天最后发现就是某个agent在分析端口里把transaction对象直接透传出去后面三个模块各自往里写字段结果共享对象被改了七八遍谁改的都不知道。后来把所有对外接口改成返回clone世界清净了。5.5 最后再分享一个小技巧在调试对象复制问题时可以给类写一个简单的描述方法打印当前对象的标识信息和关键成员virtual function string to_string(); $sformatf(packet%0d addr%0h data%0h name%s pld_ptr%0d, this, addr, data, name, pld); endfunction两个对象打印出来如果packet后面的编号不同说明外层对象不同如果pld_ptr后面数字相同说明内层对象还是共享的。这个技巧帮我快速定位过好几次浅拷贝问题。写验证环境时别嫌麻烦把这类基础方法一次性配齐后面能省很多事。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

图片被WPS截胡还卡打印?三步夺回默认关联与打印通道 2026/10/1 14:35:42

图片被WPS截胡还卡打印?三步夺回默认关联与打印通道

打开一张图片想打印,结果WPS直接弹窗告诉你:需要会员。更要命的是,你根本没想用WPS看图——它自己把你的默认打开方式接管了,双击任意一张照片,出来的永远是那个带着各种付费指引的界面。这情况我身边不下三个人遇到过…

阅读更多 →
CrossFormer实战:跨尺度注意力图像分类微调指南 2026/10/1 14:35:42

CrossFormer实战:跨尺度注意力图像分类微调指南

简介:这份资源面向希望上手视觉Transformer的开发者与图像分类学习者,围绕CrossFormer这一引入跨尺度注意力机制的新型架构,提供从模型实现到分类任务落地的完整实战素材。压缩包共2000个文件,约835.34MB,其中1986个pn…

阅读更多 →
换成 HTTP/3,弱网就能变好吗? 2026/10/1 14:35:30

换成 HTTP/3,弱网就能变好吗?

页面标题已经出来了,图片还空着,评论区也一直在转。检查网络,发现有丢包。讨论到最后,有人提议:“换 HTTP/3 吧,弱网下表现会更好。” 这个方向有依据,但还少了半句话:原来的等待&am…

阅读更多 →
又发现一个Claude Code开源神器!用Happy Coder把移动端接进TaoToken 2026/10/1 14:35:30

又发现一个Claude Code开源神器!用Happy Coder把移动端接进TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
API Testing 一个基于 YAML 文件的开源接口测试工具:从 VS Code 到 gRPC 的落地实践 2026/10/1 14:35:30

API Testing 一个基于 YAML 文件的开源接口测试工具:从 VS Code 到 gRPC 的落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenClaw Windows 搭建教程:从 WSL2 到 PowerShell 的完整配置流程 2026/10/1 14:35:29

OpenClaw Windows 搭建教程:从 WSL2 到 PowerShell 的完整配置流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉